[PATCH] Add pg_get_tablespace_ddl() function to reconstruct CREATE TABLESPACE statement
Hello!
I am submitting a patch as a part of a larger Retail DDL functions project
described by Andrew Dunstan here:
/messages/by-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.net
This patch creates a function pg_get_tablespace_ddl, designed to retrieve
the full DDL statement for a tablespace. Users can obtain the DDL by
providing the tablespace name, like so:
SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
pg_get_tablespace_ddl
---------------------------------------------------------------------------------------------------
CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION ''
WITH (random_page_cost = 3);
This patch includes documentation, comments, and regression tests, all of
which pass successfully.
--
Best,
Manni Wood
EnterpriseDB
Attachments:
v1-0001-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From a5724c200748993ea6e1e22948961a5c0f821f9d Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Tue, 28 Oct 2025 17:36:10 -0500
Subject: [PATCH v1] Adds pg_get_tablespace_ddl function
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/utils/adt/ruleutils.c | 118 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 3 +
src/test/regress/expected/tablespace.out | 59 ++++++++++++
src/test/regress/sql/tablespace.sql | 41 ++++++++
5 files changed, 266 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..79ef14c6b2f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..6b8aedc2115 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -22,6 +22,7 @@
#include "access/amapi.h"
#include "access/htup_details.h"
#include "access/relation.h"
+#include "access/reloptions.h"
#include "access/table.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
@@ -35,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -67,6 +69,7 @@
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
+#include "utils/spccache.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
#include "utils/varlena.h"
@@ -13738,3 +13741,118 @@ get_range_partbound_string(List *bound_datums)
return buf->data;
}
+
+/*
+ * pg_get_tablespace_ddl - Get CREATE TABLESPACE statement for a tablespace
+ */
+Datum
+pg_get_tablespace_ddl(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspaceoid;
+ Oid tspowneroid;
+ Datum datumLocation;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ TableSpaceOpts *opts = NULL;
+ Form_pg_tablespace tspForm;
+
+ /* Get the OID of the tablespace; we need it to find the tablespace */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ Assert(HeapTupleIsValid(tuple));
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(*tspname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ datumLocation = DirectFunctionCall1(pg_tablespace_location, tspaceoid);
+ path = text_to_cstring(DatumGetTextP(datumLocation));
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ bytea *bytea_opts = tablespace_reloptions(datum, false);
+
+ opts = (TableSpaceOpts *) palloc0(VARSIZE(bytea_opts));
+ memcpy(opts, bytea_opts, VARSIZE(bytea_opts));
+
+ /* Add the valid options in WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ if (opts->random_page_cost > 0)
+ appendStringInfo(&buf, "random_page_cost = %g, ",
+ opts->random_page_cost);
+
+ if (opts->seq_page_cost > 0)
+ appendStringInfo(&buf, "seq_page_cost = %g, ",
+ opts->seq_page_cost);
+
+ if (opts->effective_io_concurrency > 0)
+ appendStringInfo(&buf, "effective_io_concurrency = %d, ",
+ opts->effective_io_concurrency);
+
+ if (opts->maintenance_io_concurrency > 0)
+ appendStringInfo(&buf, "maintenance_io_concurrency = %d, ",
+ opts->maintenance_io_concurrency);
+
+ /* buf ends up with unwanted trailing comma and space; remove them */
+ buf.len -= 2;
+ buf.data[buf.len] = '\0'; /* Null-terminate the modified string */
+
+ /* Free the opts now */
+ pfree(opts);
+
+ appendStringInfoChar(&buf, ')');
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..022c3493542 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,9 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..8a1c678b1db 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,62 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.6, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (random_page_cost = 1.6, seq_page_cost = 1.5, effective_io_concurrency = 17, maintenance_io_concurrency = 18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..2a478558c25 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,44 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.6, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.43.0
Hi Manni,
Thanks for the patch!
On 29/10/2025 02:23, Manni Wood wrote:
This patch creates a function pg_get_tablespace_ddl, designed to
retrieve the full DDL statement for a tablespace. Users can obtain the
DDL by providing the tablespace name, like so:SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
pg_get_tablespace_ddl
---------------------------------------------------------------------------------------------------
CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION
'' WITH (random_page_cost = 3);
Here my first comments regarding usability:
== quoted identifier ==
Tablespace names containing quoted identifiers cannot be parsed:
postgres=# CREATE TABLESPACE "My TS" LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('"My TS"');
ERROR: tablespace ""My TS"" does not exist
The following works, but I guess it shouldn't:
postgres=# SELECT pg_get_tablespace_ddl('My TS');
pg_get_tablespace_ddl
-----------------------------------------------
CREATE TABLESPACE "My TS" LOCATION '/tmp/ts';
(1 row)
The same applies for unicode characters:
postgres=# CREATE TABLESPACE "🐘" LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('"🐘"');
ERROR: tablespace ""🐘"" does not exist
postgres=# SELECT pg_get_tablespace_ddl('🐘');
pg_get_tablespace_ddl
--------------------------------------------
CREATE TABLESPACE "🐘" LOCATION '/tmp/ts';
(1 row)
== option precision ==
There is a precision loss in the options:
postgres=# CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH
(seq_page_cost = 1.12345678910, random_page_cost = 1.12345678910,
effective_io_concurrency = 17, maintenance_io_concurrency = 18);
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('ts');
pg_get_tablespace_ddl
---------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------
CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH (random_page_cost
= 1.12346, seq_page_cost = 1.12346, effective_io_concurrency = 17, m
aintenance_io_concurrency = 18);
(1 row)
\db shows it as in the CREATE TABLESPACE statement:
postgres=# \db+ ts
List of tablespaces
Name | Owner | Location | Access privileges |
Options
| Size | Description
------+-------+----------+-------------------+-----------------------------------------------------------------------------------------------
-------------------------+---------+-------------
ts | u1 | /tmp/ts | |
{seq_page_cost=1.12345678910,random_page_cost=1.12345678910,effective_io_concurrency=17,mainte
nance_io_concurrency=18} | 0 bytes |
(1 row)
== permissions ==
Is it supposed to be visible to all users?
postgres=# CREATE USER u1;
CREATE ROLE
postgres=# CREATE TABLESPACE ts LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SET ROLE u1;
SET
postgres=> SELECT pg_get_tablespace_ddl('ts');
pg_get_tablespace_ddl
----------------------------------------------------
CREATE TABLESPACE ts OWNER jim LOCATION '/tmp/ts';
(1 row)
Note that \db does not allow it:
postgres=> SELECT CURRENT_USER;
current_user
--------------
u1
(1 row)
postgres=> \db+ ts
ERROR: permission denied for tablespace ts
Best, Jim
On Fri, Oct 31, 2025 at 10:36 AM Jim Jones <jim.jones@uni-muenster.de>
wrote:
Hi Manni,
Thanks for the patch!
On 29/10/2025 02:23, Manni Wood wrote:
This patch creates a function pg_get_tablespace_ddl, designed to
retrieve the full DDL statement for a tablespace. Users can obtain the
DDL by providing the tablespace name, like so:SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
pg_get_tablespace_ddl---------------------------------------------------------------------------------------------------
CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION
'' WITH (random_page_cost = 3);Here my first comments regarding usability:
== quoted identifier ==
Tablespace names containing quoted identifiers cannot be parsed:
postgres=# CREATE TABLESPACE "My TS" LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('"My TS"');
ERROR: tablespace ""My TS"" does not existThe following works, but I guess it shouldn't:
postgres=# SELECT pg_get_tablespace_ddl('My TS');
pg_get_tablespace_ddl
-----------------------------------------------
CREATE TABLESPACE "My TS" LOCATION '/tmp/ts';
(1 row)The same applies for unicode characters:
postgres=# CREATE TABLESPACE "🐘" LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('"🐘"');
ERROR: tablespace ""🐘"" does not exist
postgres=# SELECT pg_get_tablespace_ddl('🐘');
pg_get_tablespace_ddl
--------------------------------------------
CREATE TABLESPACE "🐘" LOCATION '/tmp/ts';
(1 row)== option precision ==
There is a precision loss in the options:
postgres=# CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH
(seq_page_cost = 1.12345678910, random_page_cost = 1.12345678910,
effective_io_concurrency = 17, maintenance_io_concurrency = 18);
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('ts');pg_get_tablespace_ddl
---------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------
CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH (random_page_cost
= 1.12346, seq_page_cost = 1.12346, effective_io_concurrency = 17, m
aintenance_io_concurrency = 18);
(1 row)\db shows it as in the CREATE TABLESPACE statement:
postgres=# \db+ ts
List of tablespaces
Name | Owner | Location | Access privileges |
Options
| Size | Description------+-------+----------+-------------------+-----------------------------------------------------------------------------------------------
-------------------------+---------+-------------
ts | u1 | /tmp/ts | |{seq_page_cost=1.12345678910,random_page_cost=1.12345678910,effective_io_concurrency=17,mainte
nance_io_concurrency=18} | 0 bytes |
(1 row)== permissions ==
Is it supposed to be visible to all users?
postgres=# CREATE USER u1;
CREATE ROLE
postgres=# CREATE TABLESPACE ts LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SET ROLE u1;
SET
postgres=> SELECT pg_get_tablespace_ddl('ts');
pg_get_tablespace_ddl
----------------------------------------------------
CREATE TABLESPACE ts OWNER jim LOCATION '/tmp/ts';
(1 row)Note that \db does not allow it:
postgres=> SELECT CURRENT_USER;
current_user
--------------
u1
(1 row)postgres=> \db+ ts
ERROR: permission denied for tablespace tsBest, Jim
Hi, Jim
Thanks for reviewing my very first patch!
== quoted identifier ==
I see that Postgres already has the SQL function has_tablespace_privilege
that behaves the same way as this patch's pg_get_tablespace_ddl.
# create tablespace "My TS" location '/tmp/has_space';
CREATE TABLESPACE
# select has_tablespace_privilege('My TS', 'create'); rollback;
┌──────────────────────────┐
│ has_tablespace_privilege │
├──────────────────────────┤
│ t │
└──────────────────────────┘
(1 row)
# select has_tablespace_privilege('"My TS"', 'create'); rollback;
ERROR: 42704: tablespace ""My TS"" does not exist
# create tablespace "🐘" location '/tmp/has_elephant';
CREATE TABLESPACE
# select has_tablespace_privilege('🐘', 'create'); rollback;
┌──────────────────────────┐
│ has_tablespace_privilege │
├──────────────────────────┤
│ t │
└──────────────────────────┘
(1 row)
# select has_tablespace_privilege('"🐘"', 'create'); rollback;
ERROR: 42704: tablespace ""🐘"" does not exist
Does the existence of this behavior in an existing function make the same
behavior less surprising for this patch's function?
== option precision ==
Thanks for pointing this out.
I have attached a v2 of the patch that just uses the original text the user
entered for the spcoptions.
This is much better, and it made the code smaller.
I have added "1.1234567890" to one of the tests to show that this works.
== permissions ==
I'm not sure what to think of this. psql's "\db+" does not let me show the
tablespace.
But if, as user 'u1', I select from pg_tablespace directly, I have the
permissions to do so:
postgres> select current_user; rollback;
┌──────────────┐
│ current_user │
├──────────────┤
│ u1 │
└──────────────┘
(1 row)
postgres> select * from pg_catalog.pg_tablespace; rollback;
┌───────┬────────────┬──────────┬────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ oid │ spcname │ spcowner │ spcacl │
spcoptions
│
├───────┼────────────┼──────────┼────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 1663 │ pg_default │ 10 │ [NULL] │ [NULL]
│
│ 1664 │ pg_global │ 10 │ [NULL] │ [NULL]
│
│ 19971 │ ts │ 10 │ [NULL] │
{seq_page_cost=1.12345678910,random_page_cost=1.12345678910,effective_io_concurrency=17,maintenance_io_concurrency=18}
│
└───────┴────────────┴──────────┴────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(3 rows)
So if the information is obtainable by selecting from
pg_catalog.pg_tablespace, it seems defensible to make the same data
available via pg_get_tablespace_ddl.
Thoughts?
Thanks again for reviewing my patch,
-Manni
Attachments:
v2-0001-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From 7a230ef1cac34988dbbae2b9ba6c401e149caad5 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Mon, 3 Nov 2025 17:11:00 -0600
Subject: [PATCH v2] Adds pg_get_tablespace_ddl function
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/utils/adt/ruleutils.c | 113 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 3 +
src/test/regress/expected/tablespace.out | 68 ++++++++++++++
src/test/regress/sql/tablespace.sql | 46 +++++++++
5 files changed, 275 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..79ef14c6b2f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..85b55810ddf 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -22,6 +22,7 @@
#include "access/amapi.h"
#include "access/htup_details.h"
#include "access/relation.h"
+#include "access/reloptions.h"
#include "access/table.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
@@ -35,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -67,6 +69,7 @@
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
+#include "utils/spccache.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
#include "utils/varlena.h"
@@ -13738,3 +13741,113 @@ get_range_partbound_string(List *bound_datums)
return buf->data;
}
+
+/*
+ * pg_get_tablespace_ddl - Get CREATE TABLESPACE statement for a tablespace
+ */
+Datum
+pg_get_tablespace_ddl(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspaceoid;
+ Oid tspowneroid;
+ Datum datumLocation;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+ ArrayType *optArray;
+ Datum *optDatums;
+ int optCount;
+ char *optCStr;
+
+ /* Get the OID of the tablespace; we need it to find the tablespace */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ Assert(HeapTupleIsValid(tuple));
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(*tspname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ datumLocation = DirectFunctionCall1(pg_tablespace_location, tspaceoid);
+ path = text_to_cstring(DatumGetTextP(datumLocation));
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ // NEW
+ if (!isNull)
+ {
+ optArray = DatumGetArrayTypeP(datum);
+
+ /* parseRelOptionsInternal() provided inspiration here */
+ deconstruct_array_builtin(optArray, TEXTOID,
+ &optDatums, NULL, &optCount);
+
+ if (optCount > 0)
+ {
+ appendStringInfoString(&buf, " WITH (");
+ for (int i = 0; i < optCount; i++)
+ {
+ optCStr = TextDatumGetCString(optDatums[i]);
+ appendStringInfoString(&buf, optCStr);
+ appendStringInfoString(&buf, ", ");
+ }
+ /* buf ends up with unwanted trailing comma and space; remove them */
+ buf.len -= 2;
+ buf.data[buf.len] = '\0'; /* Null-terminate the modified string */
+
+ appendStringInfoChar(&buf, ')');
+ }
+
+ pfree(optDatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..022c3493542 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,9 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..ef472f8adb9 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,71 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+ pg_get_tablespace_ddl
+------------------------------------------------
+ CREATE TABLESPACE "regress tblsp" LOCATION '';
+(1 row)
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..db3a3705065 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,49 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.43.0
On 04/11/2025 00:49, Manni Wood wrote:
== quoted identifier ==
I see that Postgres already has the SQL
function has_tablespace_privilege that behaves the same way as this
patch's pg_get_tablespace_ddl.
You're right. The source of my confusion is that I was approaching the
tablespace name as if it were a relation:
postgres=# CREATE TABLE "T"();
CREATE TABLE
postgres=# SELECT '"T"'::regclass::oid;
oid
-------
47766
(1 row)
postgres=# SELECT 'T'::regclass::oid;
ERROR: relation "t" does not exist
LINE 1: SELECT 'T'::regclass::oid;
But I see that other functions behave similarly, e.g. pg_tablespace_size:
postgres=# SELECT pg_tablespace_size('My TS');
pg_tablespace_size
--------------------
0
(1 row)
postgres=# SELECT pg_tablespace_size('"My TS"');
ERROR: tablespace ""My TS"" does not exist
postgres=#
Sorry for the noise.
Do you think that an overload in pg_proc.dat with oid as parameter would
make sense here? e.g.
{ oid => '2322',
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_tablespace_size_oid' },
{ oid => '2323',
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
== option precision ==
Thanks for pointing this out.
I have attached a v2 of the patch that just uses the original text the
user entered for the spcoptions.
Nice. It now shows the options without precision loss:
postgres=# CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH
(seq_page_cost = 1.12345678910, random_page_cost = 1.12345678910,
effective_io_concurrency = 17, maintenance_io_concurrency = 18);
CREATE TABLESPACE
postgres=# SELECT pg_get_tablespace_ddl('ts');
pg_get_tablespace_ddl
-------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------
CREATE TABLESPACE ts OWNER u1 LOCATION '/tmp/ts' WITH
(seq_page_cost=1.12345678910, random_page_cost=1.12345678910,
effective_io_concurrency=17, maintenance_io_concurrency=18);
(1 row)
postgres=# \db+ ts
List of tablespaces
Name | Owner | Location | Access privileges |
Options
| Size | Description
------+-------+----------+-------------------+---------------------------------------------------------------------------------------------
---------------------------+---------+-------------
ts | u1 | /tmp/ts | |
{seq_page_cost=1.12345678910,random_page_cost=1.12345678910,effective_io_concurrency=17,maintenance_io_concurrency=18}
| 0 bytes |
(1 row)
== permissions ==
I'm not sure what to think of this. psql's "\db+" does not let me show
the tablespace.
Right. I guess the difference here is that \db+ also shows the
tablespace's size, which requires the user to actually read it.
postgres=# CREATE TABLESPACE ts OWNER jim LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# SELECT pg_tablespace_size('ts');
pg_tablespace_size
--------------------
0
(1 row)
postgres=# SET ROLE u1;
SET
postgres=> SELECT pg_tablespace_size('ts');
ERROR: permission denied for tablespace ts
Since pg_get_tablespace_ddl doesn't display size, I believe it's fine as-is.
Thanks.
Best, Jim
On Tue, Nov 4, 2025 at 1:58 PM Jim Jones <jim.jones@uni-muenster.de> wrote:
Do you think that an overload in pg_proc.dat with oid as parameter would
make sense here? e.g.{ oid => '2322',
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype =>
'int8',
proargtypes => 'oid', prosrc => 'pg_tablespace_size_oid' },
{ oid => '2323',
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype =>
'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },Using name as parameter is more user friendly than OID.
Because users usually do not know the oids. Constructing
the DDL from the name appears better as it contains a name
in it. So, no gain in having an OID version of
pg_get_tablespace_ddl.
PFA, v3 patch set. It has some cosmetic changes and few
improvements in the new code added by Manni in v2. Also, the
new test case added did not have a DROP statement for the
tablespace created, which caused make-world failure. So, I
corrected that too.
Regards,
Nishant Sharma.
EDB, Pune.
Attachments:
v3-0001-Adds-pg_get_tablespace_ddl-function.patchapplication/octet-stream; name=v3-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From c751c0c6301ba371f96825025407dcaccb889e76 Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Tue, 4 Nov 2025 15:40:10 +0530
Subject: [PATCH v3] Adds pg_get_tablespace_ddl function
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones
Discussion: https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 ++++++++++
src/backend/utils/adt/ruleutils.c | 110 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 3 +
src/test/regress/expected/tablespace.out | 69 ++++++++++++++
src/test/regress/sql/tablespace.sql | 47 ++++++++++
5 files changed, 274 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..79ef14c6b2f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..89c977be3b5 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13738,3 +13739,112 @@ get_range_partbound_string(List *bound_datums)
return buf->data;
}
+
+/*
+ * pg_get_tablespace_ddl - Get CREATE TABLESPACE statement for a tablespace
+ */
+Datum
+pg_get_tablespace_ddl(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspaceoid;
+ Oid tspowneroid;
+ Datum datumlocation;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Get the OID of the tablespace; we need it to find the tablespace */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ Assert(HeapTupleIsValid(tuple));
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(*tspname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ datumlocation = DirectFunctionCall1(pg_tablespace_location, tspaceoid);
+ path = text_to_cstring(DatumGetTextP(datumlocation));
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..022c3493542 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,9 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..c2fed08acf7 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+ pg_get_tablespace_ddl
+------------------------------------------------
+ CREATE TABLESPACE "regress tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..aca170992d6 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+DROP TABLESPACE "regress tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
Hi Nishant
On 04/11/2025 11:37, Nishant Sharma wrote:
Using name as parameter is more user friendly than OID.
Because users usually do not know the oids. Constructing
the DDL from the name appears better as it contains a name
in it. So, no gain in having an OID version of
pg_get_tablespace_ddl.
Would you also say that having a pg_tablespace_size(oid) has no benefit?
I took a look at similar functions, and the only pattern I could
identify is that all of them take an oid parameter.
pg_tablespace_size: oid and name
pg_tablespace_location: oid
has_tablespace_privilege: oid, name, and text
pg_tablespace_databases: oid
...
pg_get_tablespace_ddl: name
I'm definitely not opposed to having just a name parameter, but I
thought it would be worth mentioning.
Best, Jim
On Tue, Nov 4, 2025 at 5:25 AM Jim Jones <jim.jones@uni-muenster.de> wrote:
Hi Nishant
On 04/11/2025 11:37, Nishant Sharma wrote:
Using name as parameter is more user friendly than OID.
Because users usually do not know the oids. Constructing
the DDL from the name appears better as it contains a name
in it. So, no gain in having an OID version of
pg_get_tablespace_ddl.Would you also say that having a pg_tablespace_size(oid) has no benefit?
I took a look at similar functions, and the only pattern I could
identify is that all of them take an oid parameter.pg_tablespace_size: oid and name
pg_tablespace_location: oid
has_tablespace_privilege: oid, name, and text
pg_tablespace_databases: oid
...
pg_get_tablespace_ddl: nameI'm definitely not opposed to having just a name parameter, but I
thought it would be worth mentioning.Best, Jim
Hello, Jim and Nishant!
About having an OID variant:
I definitely want to keep the current name-based parameter, and it looks
like we all agree on that.
The question is if we should additionally have an OID-based variant.
I personally see no harm in additionally having an OID-based variant,
seeing as it looks like a lot of functions do seem to take an OID. If I
understand correctly, many functions take an OID, and Postgres users are
supposed to have read the docs (
https://www.postgresql.org/docs/current/datatype-oid.html) to know to cast
names to OIDs. So, in terms of following established practice / patterns,
an OID-based variant is defensible.
Thankfully for people like me (for whom the "just cast the OID to a name"
pattern never sunk in after 25 years of using Postgres), I'm glad text/name
variants of functions are also a thing in Postgres, as I suspect every time
a user has a choice between the two, a user will choose to just provide the
name.
Let me know what you think!
Thanks, Jim,
Thanks Nishant for fixing/improving my v2 patch to v3!
-Manni
On 04/11/2025 15:19, Manni Wood wrote:
I personally see no harm in additionally having an OID-based variant,
seeing as it looks like a lot of functions do seem to take an OID. If I
understand correctly, many functions take an OID, and Postgres users are
supposed to have read the docs (https://www.postgresql.org/docs/current/
datatype-oid.html <https://www.postgresql.org/docs/current/datatype-
oid.html>) to know to cast names to OIDs. So, in terms of following
established practice / patterns, an OID-based variant is defensible.
+1
That's the way I see it too. Of course it's always easier to use the
tablespace's name, but there might be cases where you only have the oid
-- in which case you'd need to do a JOIN with pg_tablespace to find the
name. It's just for convenience.
Best, Jim
On 2025-Nov-04, Jim Jones wrote:
That's the way I see it too. Of course it's always easier to use the
tablespace's name, but there might be cases where you only have the oid
-- in which case you'd need to do a JOIN with pg_tablespace to find the
name. It's just for convenience.
The other DDL-producing patches that are being posted, all depend on a
reg* type for their argument, which means they will work correctly with
either an OID or an object name. Tablespaces are one of the few object
types for which no "regtablespace" exists, so I think it's fair to
require both forms.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
[…] indem ich in meinem Leben oft an euch gedacht, euch glücklich zu machen. Seyd es!
A menudo he pensado en vosotros, en haceros felices. ¡Sedlo, pues!
Heiligenstädter Testament, L. v. Beethoven, 1802
https://de.wikisource.org/wiki/Heiligenstädter_Testament
On Wed, Nov 5, 2025 at 12:56 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-04, Jim Jones wrote:
That's the way I see it too. Of course it's always easier to use the
tablespace's name, but there might be cases where you only have the oid
-- in which case you'd need to do a JOIN with pg_tablespace to find the
name. It's just for convenience.The other DDL-producing patches that are being posted, all depend on a
reg* type for their argument, which means they will work correctly with
either an OID or an object name. Tablespaces are one of the few object
types for which no "regtablespace" exists, so I think it's fair to
require both forms.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
[…] indem ich in meinem Leben oft an euch gedacht, euch glücklich zu
machen. Seyd es!
A menudo he pensado en vosotros, en haceros felices. ¡Sedlo, pues!
Heiligenstädter Testament, L. v. Beethoven, 1802
https://de.wikisource.org/wiki/Heiligenstädter_Testament
My reasons why I thought only name form was sufficient:-
1. The use case that I had in my mind for this get DDL function was
getting covered with name as its parameter. As we are creating DDL
and name will be part of it. Hence using it as input to our function to
create its DDL.
2. As Álvaro mentioned, we don't have any regtablespace for
tablespaces, So, using <tablespacename>::regtablespace::oid is not
a case for this get_ddl. But is valid for other get_ddl funcs. And even
for them we use the name in the form <objectname>::reg<object>::oid
and internally the get_ddl gets OID. The user again here does not
worry about the OIDs of their <objectname>.
3. As Manni mentions, regarding casting names to oid. But that is not
valid for tablespaces currently. If I am not missing anything. I think
users would explicitly need to provide OID to this function as a value
or from some "select oid ...".
4. The list of other tablespaces functions shared by Jim has two
functions, pg_tablespace_location() & pg_tablespace_databases()
that takes only oid as parameter and not name or text (maybe would
have been better), why? I am not sure, maybe the use case at that
time needed only an oid variant?
But yeah, with the current panel we have a majority here for having the
OID variant for this function. And of course there is no harm with it.
So, PFA v4 patch set. I have included the OID variant in it.
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.com
Attachments:
v4-0001-Adds-pg_get_tablespace_ddl-function.patchapplication/octet-stream; name=v4-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From 91cbdd8d85617b2c9f9ed6b8c5cf1e036911baf6 Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Wed, 5 Nov 2025 11:46:49 +0530
Subject: [PATCH v4] Adds pg_get_tablespace_ddl function
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 ++++++++
src/backend/utils/adt/ruleutils.c | 137 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/tablespace.out | 69 ++++++++++++
src/test/regress/sql/tablespace.sql | 47 ++++++++
5 files changed, 304 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..79ef14c6b2f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5398679cce2..a3b0dcfdca1 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -546,6 +547,7 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static text *build_tablespace_ddl(const Oid tspaceoid);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13742,3 +13744,138 @@ get_range_partbound_string(List *bound_datums)
return buf->data;
}
+
+/*
+ * build_tablespace_ddl - Build CREATE TABLESPACE statement for a
+ * tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+static text *
+build_tablespace_ddl(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datumlocation;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ datumlocation = DirectFunctionCall1(pg_tablespace_location, tspaceoid);
+ path = text_to_cstring(DatumGetTextP(datumlocation));
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return string_to_text(buf.data);
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_TEXT_P(build_tablespace_ddl(get_tablespace_oid(NameStr(*(Name)PG_GETARG_NAME(0)),
+ false)));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_TEXT_P(build_tablespace_ddl((Oid)PG_GETARG_OID(0)));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..bbc59e73537 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..c2fed08acf7 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+ pg_get_tablespace_ddl
+------------------------------------------------
+ CREATE TABLESPACE "regress tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..aca170992d6 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+DROP TABLESPACE "regress tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
On 2025-Nov-05, Nishant Sharma wrote:
My reasons why I thought only name form was sufficient:-
1. The use case that I had in my mind for this get DDL function was
getting covered with name as its parameter. As we are creating DDL
and name will be part of it. Hence using it as input to our function to
create its DDL.
Accepting an OID as parameter lets the user call this function in a
query that returns a tablespace OID as parameter, without having to add
a join to pg_tablespace. Not something you see very frequently, but I
can imagine GUI tool writers doing that.
4. The list of other tablespaces functions shared by Jim has two
functions, pg_tablespace_location() & pg_tablespace_databases()
that takes only oid as parameter and not name or text (maybe would
have been better), why? I am not sure, maybe the use case at that
time needed only an oid variant?
Lack of the other form of pg_tablespace_location has annoyed me on
occassion, but I don't think it's frequent enough to request it to be
added. (I don't remember ever having a need to call
pg_tablespace_databases).
+ /* Find tablespace directory path */ + datumlocation = DirectFunctionCall1(pg_tablespace_location, tspaceoid); + path = text_to_cstring(DatumGetTextP(datumlocation));
It seems worth splitting pg_tablespace_location in two parts: one outer
shell that takes PG_FUNCTION_ARGS and returns text, and an inner
implementation function that takes a plain Oid and returns char *. This
way, you can use the inner one here without the DirectFunctionCall1()
scaffolding, and avoid having to convert the laboriously constructed
text immediately back to a C string.
+Datum +pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(build_tablespace_ddl(get_tablespace_oid(NameStr(*(Name)PG_GETARG_NAME(0)), + false))); +}
This line is far too clever. Better add a Name variable for
PG_GETARG_NAME(), an Oid variable for get_tablespace_oid(), and so on.
It'll be more code lines, but they will be ten times more readable.
+Datum +pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(build_tablespace_ddl((Oid)PG_GETARG_OID(0))); +}
This one isn't _that_ bad -- still, our style is to have all the
PG_GETARG_foo() invocations together in an orderly fashion at the very
top of local variable declarations in pretty much all of our functions,
and I think that's a good habit to keep.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Digital and video cameras have this adjustment and film cameras don't for the
same reason dogs and cats lick themselves: because they can." (Ken Rockwell)
Thanks Álvaro for the review comments on v4!
PFA, v5 patch set. I have included all your review comments.
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.com
Attachments:
v5-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchapplication/octet-stream; name=v5-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchDownload
From d6a213bcbf2c630f9000d14d2f23065d8017e6db Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Wed, 5 Nov 2025 16:19:26 +0530
Subject: [PATCH v5 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion: https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/utils/adt/misc.c | 62 ++----------------------
src/backend/utils/adt/ruleutils.c | 79 +++++++++++++++++++++++++++++++
src/include/utils/ruleutils.h | 2 +
3 files changed, 85 insertions(+), 58 deletions(-)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5398679cce2..976a8345ce4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -18,6 +18,7 @@
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
+#include <sys/stat.h>
#include "access/amapi.h"
#include "access/htup_details.h"
@@ -13742,3 +13743,81 @@ get_range_partbound_string(List *bound_datums)
return buf->data;
}
+
+/*
+ * get_tablespace_loc_string - get location for a tablespace in string. This is
+ * internal version which helps pg_tablespace_location() and others.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = { '\0' };
+ char targetpath[MAXPGPATH] = { '\0' };
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..6d74485f32a 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -54,4 +54,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* RULEUTILS_H */
--
2.47.3
v5-0002-Adds-pg_get_tablespace_ddl-function.patchapplication/octet-stream; name=v5-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From 4e9bb52c032382a7793037823ff3c8d06ea9d74a Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Wed, 5 Nov 2025 16:31:56 +0530
Subject: [PATCH v5 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion: https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++
src/backend/utils/adt/ruleutils.c | 151 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++
6 files changed, 319 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..79ef14c6b2f 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 976a8345ce4..9dd13d19fd6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -36,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13821,3 +13822,153 @@ get_tablespace_loc_string(Oid tablespaceOid)
return buf.data;
}
+
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement for
+ * a tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..ece0610b558 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 6d74485f32a..e709eafccfd 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -55,5 +55,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
extern char *get_tablespace_loc_string(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..c2fed08acf7 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+ pg_get_tablespace_ddl
+------------------------------------------------
+ CREATE TABLESPACE "regress tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..aca170992d6 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress tblsp');
+DROP TABLESPACE "regress tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
On Wed, Nov 5, 2025 at 5:54 AM Nishant Sharma <
nishant.sharma@enterprisedb.com> wrote:
Thanks Álvaro for the review comments on v4!
PFA, v5 patch set. I have included all your review comments.
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.com
The BSD build was failing with the error 'WARNING: tablespaces created by
regression test cases should have names starting with "regress_"', so the
attached patches should fix that.
The windows build is also failing, on this error
"../src/port/strerror.c(311): fatal error C1051: program database file,
'C:\cirrus\build\src\port\libpgport_srv.pdb', has an obsolete format,
delete it and recompile", which I don't think is related to our patch.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v6-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchtext/x-patch; charset=UTF-8; name=v6-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchDownload
From 11831687a0324aa88d13716ec7f051a80a42f8e1 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Thu, 6 Nov 2025 14:03:02 -0600
Subject: [PATCH v6 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/utils/adt/misc.c | 62 ++----------------------
src/backend/utils/adt/ruleutils.c | 79 +++++++++++++++++++++++++++++++
src/include/utils/ruleutils.h | 2 +
3 files changed, 85 insertions(+), 58 deletions(-)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..b15d85c5477 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -18,6 +18,7 @@
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
+#include <sys/stat.h>
#include "access/amapi.h"
#include "access/htup_details.h"
@@ -13743,3 +13744,81 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * get_tablespace_loc_string - get location for a tablespace in string. This is
+ * internal version which helps pg_tablespace_location() and others.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = { '\0' };
+ char targetpath[MAXPGPATH] = { '\0' };
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..6d74485f32a 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -54,4 +54,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* RULEUTILS_H */
--
2.51.2
v6-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v6-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From 1297f674c2f69aa0ec689fd65c61a45e466e9eae Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Thu, 6 Nov 2025 14:16:26 -0600
Subject: [PATCH v6 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++
src/backend/utils/adt/ruleutils.c | 151 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++
6 files changed, 319 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..74ca0ed1066 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b15d85c5477..6d1aa3b20ab 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -36,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13822,3 +13823,153 @@ get_tablespace_loc_string(Oid tablespaceOid)
return buf.data;
}
+
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement for
+ * a tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..ece0610b558 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 6d74485f32a..e709eafccfd 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -55,5 +55,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
extern char *get_tablespace_loc_string(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.51.2
On Thu, Nov 6, 2025 at 4:08 PM Manni Wood <manni.wood@enterprisedb.com>
wrote:
On Wed, Nov 5, 2025 at 5:54 AM Nishant Sharma <
nishant.sharma@enterprisedb.com> wrote:Thanks Álvaro for the review comments on v4!
PFA, v5 patch set. I have included all your review comments.
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.comThe BSD build was failing with the error 'WARNING: tablespaces created by
regression test cases should have names starting with "regress_"', so the
attached patches should fix that.The windows build is also failing, on this error
"../src/port/strerror.c(311): fatal error C1051: program database file,
'C:\cirrus\build\src\port\libpgport_srv.pdb', has an obsolete format,
delete it and recompile", which I don't think is related to our patch.
--
-- Manni Wood EDB: https://www.enterprisedb.com
And once again, I am foiled by whitespace. Attached v7 fixes problems in
tests due to whitespace.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v7-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v7-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From 84dabefcda2a5d7026280bdbaa91b3b44dd8784e Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Thu, 6 Nov 2025 19:19:50 -0600
Subject: [PATCH v7 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++
src/backend/utils/adt/ruleutils.c | 151 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++
6 files changed, 319 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..74ca0ed1066 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b15d85c5477..6d1aa3b20ab 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -36,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13822,3 +13823,153 @@ get_tablespace_loc_string(Oid tablespaceOid)
return buf.data;
}
+
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement for
+ * a tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..ece0610b558 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 6d74485f32a..e709eafccfd 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -55,5 +55,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
extern char *get_tablespace_loc_string(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.51.2
v7-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchtext/x-patch; charset=UTF-8; name=v7-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchDownload
From 4df331b743cfac263e24105bcf97c93507f80d02 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Thu, 6 Nov 2025 18:14:32 -0600
Subject: [PATCH v7 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/utils/adt/misc.c | 62 ++----------------------
src/backend/utils/adt/ruleutils.c | 79 +++++++++++++++++++++++++++++++
src/include/utils/ruleutils.h | 2 +
3 files changed, 85 insertions(+), 58 deletions(-)
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..b15d85c5477 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -18,6 +18,7 @@
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
+#include <sys/stat.h>
#include "access/amapi.h"
#include "access/htup_details.h"
@@ -13743,3 +13744,81 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * get_tablespace_loc_string - get location for a tablespace in string. This is
+ * internal version which helps pg_tablespace_location() and others.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = { '\0' };
+ char targetpath[MAXPGPATH] = { '\0' };
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..6d74485f32a 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -54,4 +54,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* RULEUTILS_H */
--
2.51.2
On 2025-Nov-05, Nishant Sharma wrote:
Thanks Álvaro for the review comments on v4!
PFA, v5 patch set. I have included all your review comments.
Great, thanks.
I think adding the get_tablespace_location_string function in
ruleutils.c makes little sense -- I would say it belongs in
src/backend/catalog/pg_tablespace.c. Since this file happens not to
exist, you can create it.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Someone said that it is at least an order of magnitude more work to do
production software than a prototype. I think he is wrong by at least
an order of magnitude." (Brian Kernighan)
On 07/11/2025 02:27, Manni Wood wrote:
Attached v7 fixes problems in tests due to whitespace.
Since get_tablespace_loc_string returns a palloc'd string, I guess you
could pfree it after the if block. The same applies for spcowner, since
you're calling GetUserNameFromId() with noerr = false.
For reference, see pg_get_indexdef_worker():
...
/*
* If it has options, append "WITH (options)"
*/
str = flatten_reloptions(indexrelid);
if (str)
{
appendStringInfo(&buf, " WITH (%s)", str);
pfree(str);
}
...
Thanks
Best, Jim
On Fri, Nov 7, 2025 at 10:16 AM Jim Jones <jim.jones@uni-muenster.de> wrote:
On 07/11/2025 02:27, Manni Wood wrote:
Attached v7 fixes problems in tests due to whitespace.
Since get_tablespace_loc_string returns a palloc'd string, I guess you
could pfree it after the if block. The same applies for spcowner, since
you're calling GetUserNameFromId() with noerr = false.For reference, see pg_get_indexdef_worker():
...
/*
* If it has options, append "WITH (options)"
*/
str = flatten_reloptions(indexrelid);
if (str)
{
appendStringInfo(&buf, " WITH (%s)", str);
pfree(str);
}
...Thanks
Best, Jim
Hello, Álvaro and Jim!
I have incorporated both of your suggestions into this pair of v8 patches.
Let me know what you think.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v8-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v8-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From 29e92da55e817c7938cf0be5bdda4bc2d420c7ca Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Fri, 7 Nov 2025 15:59:14 -0600
Subject: [PATCH v8 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++
src/backend/utils/adt/ruleutils.c | 153 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/tablespace.out | 69 ++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++
5 files changed, 320 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..74ca0ed1066 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c8023e83f4b..31ddd2ea49a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -36,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13744,3 +13745,155 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement for
+ * a tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.51.2
v8-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchtext/x-patch; charset=UTF-8; name=v8-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchDownload
From b3e23e59ff74f9def70416a65a4ebb59052c49bf Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Fri, 7 Nov 2025 15:12:18 -0600
Subject: [PATCH v8 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/catalog/Makefile | 1 +
src/backend/catalog/pg_tablespace.c | 151 ++++++++++++++++++++++++++++
src/backend/utils/adt/misc.c | 62 +-----------
src/backend/utils/adt/ruleutils.c | 1 +
src/include/catalog/pg_tablespace.h | 2 +
5 files changed, 159 insertions(+), 58 deletions(-)
create mode 100644 src/backend/catalog/pg_tablespace.c
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..8e40e1b8189 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -44,6 +44,7 @@ OBJS = \
pg_range.o \
pg_shdepend.o \
pg_subscription.o \
+ pg_tablespace.o \
pg_type.o \
storage.o \
toasting.o
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
new file mode 100644
index 00000000000..95f7c3bf6d0
--- /dev/null
+++ b/src/backend/catalog/pg_tablespace.c
@@ -0,0 +1,151 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tablespace.c
+ * routines to support tablespaces
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_tablespace.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "access/amapi.h"
+#include "access/htup_details.h"
+#include "access/relation.h"
+#include "access/table.h"
+#include "catalog/pg_aggregate.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_language.h"
+#include "catalog/pg_opclass.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/defrem.h"
+#include "commands/tablespace.h"
+#include "common/keywords.h"
+#include "executor/spi.h"
+#include "funcapi.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/pathnodes.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_agg.h"
+#include "parser/parse_func.h"
+#include "parser/parse_oper.h"
+#include "parser/parse_relation.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.h"
+#include "rewrite/rewriteSupport.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
+#include "utils/partcache.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+#include "utils/varlena.h"
+#include "utils/xml.h"
+
+/*
+ * get_tablespace_loc_string - Get a tablespace's location as a C-string.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = {'\0'};
+ char targetpath[MAXPGPATH] = {'\0'};
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..c8023e83f4b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -18,6 +18,7 @@
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
+#include <sys/stat.h>
#include "access/amapi.h"
#include "access/htup_details.h"
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 5293488c630..f065cff9ddc 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -54,4 +54,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* PG_TABLESPACE_H */
--
2.51.2
On Fri, Nov 7, 2025 at 4:38 PM Manni Wood <manni.wood@enterprisedb.com>
wrote:
On Fri, Nov 7, 2025 at 10:16 AM Jim Jones <jim.jones@uni-muenster.de>
wrote:On 07/11/2025 02:27, Manni Wood wrote:
Attached v7 fixes problems in tests due to whitespace.
Since get_tablespace_loc_string returns a palloc'd string, I guess you
could pfree it after the if block. The same applies for spcowner, since
you're calling GetUserNameFromId() with noerr = false.For reference, see pg_get_indexdef_worker():
...
/*
* If it has options, append "WITH (options)"
*/
str = flatten_reloptions(indexrelid);
if (str)
{
appendStringInfo(&buf, " WITH (%s)", str);
pfree(str);
}
...Thanks
Best, Jim
Hello, Álvaro and Jim!
I have incorporated both of your suggestions into this pair of v8 patches.
Let me know what you think.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Alas, the build https://commitfest.postgresql.org/patch/6175/ now fails,
and I cannot reproduce on my machine. Obviously there will be a v9...
--
-- Manni Wood EDB: https://www.enterprisedb.com
On 08/11/2025 00:38, Manni Wood wrote:
Alas, the build https://commitfest.postgresql.org/patch/6175/ <https://
commitfest.postgresql.org/patch/6175/> now fails, and I cannot reproduce
on my machine. Obviously there will be a v9...
You forgot the declaration for build_tablespace_ddl_string[1]:
ruleutils.c:13755:1: warning: no previous prototype for
‘build_tablespace_ddl_string’ [-Wmissing-prototypes]
13755 | build_tablespace_ddl_string(const Oid tspaceoid)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
Best, Jim
1 - https://cirrus-ci.com/task/4855404196265984?logs=build#L1911
On Fri, Nov 7, 2025 at 6:03 PM Jim Jones <jim.jones@uni-muenster.de> wrote:
On 08/11/2025 00:38, Manni Wood wrote:
Alas, the build https://commitfest.postgresql.org/patch/6175/ <https://
commitfest.postgresql.org/patch/6175/> now fails, and I cannot reproduce
on my machine. Obviously there will be a v9...You forgot the declaration for build_tablespace_ddl_string[1]:
ruleutils.c:13755:1: warning: no previous prototype for
‘build_tablespace_ddl_string’ [-Wmissing-prototypes]
13755 | build_tablespace_ddl_string(const Oid tspaceoid)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~Best, Jim
1 - https://cirrus-ci.com/task/4855404196265984?logs=build#L1911
Thank you very much, Jim. Serves me right for looking at the error at the
end of the logs rather than the warning in the middle.
v9 is attached.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v9-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v9-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From e5eccab287ca461ad943977dcc9d5bb437957866 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Fri, 7 Nov 2025 15:59:14 -0600
Subject: [PATCH v9 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++
src/backend/utils/adt/ruleutils.c | 154 +++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/tablespace.out | 69 ++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++
5 files changed, 321 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..74ca0ed1066 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c8023e83f4b..53b89fc2b03 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -36,6 +36,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -547,6 +548,7 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static char *build_tablespace_ddl_string(const Oid tspaceoid);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13744,3 +13746,155 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement for
+ * a tablespace from its OID. This is internal version which helps
+ * pg_get_tablespace_ddl_name() and pg_get_tablespace_ddl_oid().
+ */
+static char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.51.2
v9-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchtext/x-patch; charset=UTF-8; name=v9-0001-Supporting-changes-for-pg_get_tablespace_ddl-func.patchDownload
From b3e23e59ff74f9def70416a65a4ebb59052c49bf Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Fri, 7 Nov 2025 15:12:18 -0600
Subject: [PATCH v9 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/catalog/Makefile | 1 +
src/backend/catalog/pg_tablespace.c | 151 ++++++++++++++++++++++++++++
src/backend/utils/adt/misc.c | 62 +-----------
src/backend/utils/adt/ruleutils.c | 1 +
src/include/catalog/pg_tablespace.h | 2 +
5 files changed, 159 insertions(+), 58 deletions(-)
create mode 100644 src/backend/catalog/pg_tablespace.c
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..8e40e1b8189 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -44,6 +44,7 @@ OBJS = \
pg_range.o \
pg_shdepend.o \
pg_subscription.o \
+ pg_tablespace.o \
pg_type.o \
storage.o \
toasting.o
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
new file mode 100644
index 00000000000..95f7c3bf6d0
--- /dev/null
+++ b/src/backend/catalog/pg_tablespace.c
@@ -0,0 +1,151 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tablespace.c
+ * routines to support tablespaces
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_tablespace.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#include "access/amapi.h"
+#include "access/htup_details.h"
+#include "access/relation.h"
+#include "access/table.h"
+#include "catalog/pg_aggregate.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_language.h"
+#include "catalog/pg_opclass.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "catalog/pg_type.h"
+#include "commands/defrem.h"
+#include "commands/tablespace.h"
+#include "common/keywords.h"
+#include "executor/spi.h"
+#include "funcapi.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/pathnodes.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_agg.h"
+#include "parser/parse_func.h"
+#include "parser/parse_oper.h"
+#include "parser/parse_relation.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.h"
+#include "rewrite/rewriteSupport.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/lsyscache.h"
+#include "utils/partcache.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+#include "utils/varlena.h"
+#include "utils/xml.h"
+
+/*
+ * get_tablespace_loc_string - Get a tablespace's location as a C-string.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = {'\0'};
+ char targetpath[MAXPGPATH] = {'\0'};
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..c8023e83f4b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -18,6 +18,7 @@
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
+#include <sys/stat.h>
#include "access/amapi.h"
#include "access/htup_details.h"
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 5293488c630..f065cff9ddc 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -54,4 +54,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* PG_TABLESPACE_H */
--
2.51.2
On Fri, Nov 7, 2025 at 10:19 PM Manni Wood <manni.wood@enterprisedb.com>
wrote:
On Fri, Nov 7, 2025 at 6:03 PM Jim Jones <jim.jones@uni-muenster.de>
wrote:On 08/11/2025 00:38, Manni Wood wrote:
Alas, the build https://commitfest.postgresql.org/patch/6175/ <https://
commitfest.postgresql.org/patch/6175/> now fails, and I cannotreproduce
on my machine. Obviously there will be a v9...
You forgot the declaration for build_tablespace_ddl_string[1]:
ruleutils.c:13755:1: warning: no previous prototype for
‘build_tablespace_ddl_string’ [-Wmissing-prototypes]
13755 | build_tablespace_ddl_string(const Oid tspaceoid)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~Best, Jim
1 - https://cirrus-ci.com/task/4855404196265984?logs=build#L1911
Thank you very much, Jim. Serves me right for looking at the error at the
end of the logs rather than the warning in the middle.v9 is attached.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Ah, the error at the end of the logs is indeed still happening. Glad to
have gotten rid of that earlier warning, though.
https://cirrus-ci.com/task/6437176629526528?logs=build#L1906-L1912
I will ask for Nishant's help with this and post another patch.
--
-- Manni Wood EDB: https://www.enterprisedb.com
On Fri, Nov 7, 2025 at 10:46 PM Manni Wood <manni.wood@enterprisedb.com>
wrote:
On Fri, Nov 7, 2025 at 10:19 PM Manni Wood <manni.wood@enterprisedb.com>
wrote:On Fri, Nov 7, 2025 at 6:03 PM Jim Jones <jim.jones@uni-muenster.de>
wrote:On 08/11/2025 00:38, Manni Wood wrote:
Alas, the build https://commitfest.postgresql.org/patch/6175/
<https://
commitfest.postgresql.org/patch/6175/> now fails, and I cannot
reproduce
on my machine. Obviously there will be a v9...
You forgot the declaration for build_tablespace_ddl_string[1]:
ruleutils.c:13755:1: warning: no previous prototype for
‘build_tablespace_ddl_string’ [-Wmissing-prototypes]
13755 | build_tablespace_ddl_string(const Oid tspaceoid)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~Best, Jim
1 - https://cirrus-ci.com/task/4855404196265984?logs=build#L1911
Thank you very much, Jim. Serves me right for looking at the error at the
end of the logs rather than the warning in the middle.v9 is attached.
--
-- Manni Wood EDB: https://www.enterprisedb.comAh, the error at the end of the logs is indeed still happening. Glad to
have gotten rid of that earlier warning, though.https://cirrus-ci.com/task/6437176629526528?logs=build#L1906-L1912
I will ask for Nishant's help with this and post another patch.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Apologies for the noise. Just wanted to confirm that I did a fresh clone of
github.com/postgres/postgres on my linux machine, applied both v9 patches
to master, ran
"configure", "make world", "make check-world", and everything passed.
However, I see that this
https://cirrus-ci.com/task/6437176629526528?logs=clone#L214 checks out a
specific commit on a specific branch:
"Checked out db131410131cb6a60f074213b0e7aaaa15d72f87 on cf/6175 branch."
My clone of postgres (which is presumably shallow?) does not show that
branch, not even with "git branch -r".
Thanks very much, all, for your patience.
--
-- Manni Wood EDB: https://www.enterprisedb.com
On 08/11/2025 15:05, Manni Wood wrote:
Apologies for the noise. Just wanted to confirm that I did a fresh clone
of github.com/postgres/postgres <http://github.com/postgres/postgres> on
my linux machine, applied both v9 patches to master, ran
"configure", "make world", "make check-world", and everything passed.
Perhaps a missing entry at src/backend/catalog/meson.build for
pg_tablespace.c? You probably don't see the error in your environment
because you're not building with meson.
Best, Jim
1. I have moved our build_tablespace_ddl_string in pg_tablespace.c
2. Removed unnecessary includes in new file pg_tablespace.c
3. Added 'or oid' as type in doc file for documentation along with name.
4. Added 'pg_tablespace.c' in the meson build file.
The problem appears to be with meson build (also suggested by Jim):
https://cirrus-ci.com/task/5376297293053952
Hopefully this resolves the CI issue.
PFA, v10 patch set.
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.com/
Attachments:
v10-0002-Adds-pg_get_tablespace_ddl-function.patchapplication/octet-stream; name=v10-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From 30c24eef357966c929c00465465e3e111aba9285 Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Mon, 10 Nov 2025 10:54:34 +0530
Subject: [PATCH v10 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/catalog/pg_tablespace.c | 119 +++++++++++++++++++++++
src/backend/utils/adt/ruleutils.c | 37 +++++++
src/include/catalog/pg_proc.dat | 6 ++
src/include/catalog/pg_tablespace.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++++
7 files changed, 324 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..64c56e0a469 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index c00888456c8..c147c0bc575 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -16,9 +16,13 @@
#include <sys/stat.h>
+#include "access/htup_details.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
/*
@@ -98,3 +102,118 @@ get_tablespace_loc_string(Oid tablespaceOid)
return buf.data;
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement as a
+ * C-string for a tablespace from its OID.
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_loc_string(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..dc5365fbeae 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13743,3 +13744,39 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index f065cff9ddc..c78dcc91b02 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -55,5 +55,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
extern char *get_tablespace_loc_string(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* PG_TABLESPACE_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
v10-0001-Supporting-changes-for-pg_get_tablespace_ddl-fun.patchapplication/octet-stream; name=v10-0001-Supporting-changes-for-pg_get_tablespace_ddl-fun.patchDownload
From ca63d247d031e740a58f73642ce2c1806c43b18d Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Mon, 10 Nov 2025 11:22:00 +0530
Subject: [PATCH v10 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/catalog/Makefile | 1 +
src/backend/catalog/meson.build | 1 +
src/backend/catalog/pg_tablespace.c | 100 ++++++++++++++++++++++++++++
src/backend/utils/adt/misc.c | 62 ++---------------
src/include/catalog/pg_tablespace.h | 2 +
5 files changed, 108 insertions(+), 58 deletions(-)
create mode 100644 src/backend/catalog/pg_tablespace.c
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..8e40e1b8189 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -44,6 +44,7 @@ OBJS = \
pg_range.o \
pg_shdepend.o \
pg_subscription.o \
+ pg_tablespace.o \
pg_type.o \
storage.o \
toasting.o
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 1958ea9238a..58674ffeee6 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -31,6 +31,7 @@ backend_sources += files(
'pg_range.c',
'pg_shdepend.c',
'pg_subscription.c',
+ 'pg_tablespace.c',
'pg_type.c',
'storage.c',
'toasting.c',
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
new file mode 100644
index 00000000000..c00888456c8
--- /dev/null
+++ b/src/backend/catalog/pg_tablespace.c
@@ -0,0 +1,100 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tablespace.c
+ * routines to support tablespaces
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_tablespace.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <sys/stat.h>
+
+#include "catalog/pg_tablespace.h"
+#include "commands/tablespace.h"
+#include "miscadmin.h"
+
+
+/*
+ * get_tablespace_loc_string - Get a tablespace's location as a C-string for a
+ * tablespace from its OID.
+ */
+char *
+get_tablespace_loc_string(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH] = {'\0'};
+ char targetpath[MAXPGPATH] = {'\0'};
+ int rllen;
+ struct stat st;
+ StringInfoData buf;
+
+ initStringInfo(&buf);
+ appendStringInfoString(&buf, "");
+
+ /*
+ * It's useful to apply this function to pg_class.reltablespace, wherein
+ * zero means "the database's default tablespace". So, rather than
+ * throwing an error for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return buf.data;
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ {
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath)));
+ }
+
+ if (!S_ISLNK(st.st_mode))
+ {
+ appendStringInfoString(&buf, sourcepath);
+ return buf.data;
+ }
+
+ /*
+ * In presence of a link or a junction point, return the path pointing to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath)));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath)));
+ targetpath[rllen] = '\0';
+
+ appendStringInfoString(&buf, targetpath);
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..cb99d7435eb 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 5293488c630..f065cff9ddc 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -54,4 +54,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
+extern char *get_tablespace_loc_string(Oid tablespaceOid);
+
#endif /* PG_TABLESPACE_H */
--
2.47.3
On 2025-Nov-08, Manni Wood wrote:
However, I see that this
https://cirrus-ci.com/task/6437176629526528?logs=clone#L214 checks out a
specific commit on a specific branch:"Checked out db131410131cb6a60f074213b0e7aaaa15d72f87 on cf/6175 branch."
My clone of postgres (which is presumably shallow?) does not show that
branch, not even with "git branch -r".
Yeah, these branches used by CI are made up on the spot and aren't
propagated out of that repository. You can add a "remote" to your
existing clone,
git remote add cfbot https://github.com/postgresql-cfbot/postgresql.git
and then you can see the branches it creates and `git switch` to them.
Do mind that they are ephemeral, and for this thread they only contain
the patches you submitted yourself; really, this is only useful if
you're going to review other people's patches, and even then it might
still be better to use the patches from the mailing list (which is where
cfbot itself grabs them from anyway).
You should definitely be reviewing other people's patches, though, not
just because it's valuable for the community as a whole to have an
additional pair of eyes on them, but also because they can teach you
many things.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Oh, great altar of passive entertainment, bestow upon me thy discordant images
at such speed as to render linear thought impossible" (Calvin a la TV)
On Mon, Nov 10, 2025 at 5:27 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-08, Manni Wood wrote:
However, I see that this
https://cirrus-ci.com/task/6437176629526528?logs=clone#L214 checks out a
specific commit on a specific branch:"Checked out db131410131cb6a60f074213b0e7aaaa15d72f87 on cf/6175 branch."
My clone of postgres (which is presumably shallow?) does not show that
branch, not even with "git branch -r".Yeah, these branches used by CI are made up on the spot and aren't
propagated out of that repository. You can add a "remote" to your
existing clone,git remote add cfbot https://github.com/postgresql-cfbot/postgresql.git
and then you can see the branches it creates and `git switch` to them.
Do mind that they are ephemeral, and for this thread they only contain
the patches you submitted yourself; really, this is only useful if
you're going to review other people's patches, and even then it might
still be better to use the patches from the mailing list (which is where
cfbot itself grabs them from anyway).You should definitely be reviewing other people's patches, though, not
just because it's valuable for the community as a whole to have an
additional pair of eyes on them, but also because they can teach you
many things.--
Álvaro Herrera Breisgau, Deutschland —
https://www.EnterpriseDB.com/
"Oh, great altar of passive entertainment, bestow upon me thy discordant
images
at such speed as to render linear thought impossible" (Calvin a la TV)
Thank you very much, Nishant, Jim, and Álvaro!
I will start building with both make and Meson before submitting patches.
Lesson learned.
Also, Álvaro, yes, I do need to start reviewing patches this week.
Thanks, all!
--
-- Manni Wood EDB: https://www.enterprisedb.com
On 2025-Nov-10, Nishant Sharma wrote:
PFA, v10 patch set.
I propose the following changes for 0001, in patch hunk ordering.
1. pg_tablespace_location was introduced in 2011 (commit 16d8e594acd9),
so claim copyright starting at that point.
2. readlink(2) claims, at least in my system, to need <unistd.h>. Add
that.
3. get_tablespace_loc_string() is such an ugly name. Why not
get_tablespace_location()?
3. The initialization of sourcepath and targetpath are mostly pointless
(see below), so I'd leave it out.
3a. (Also, it's not clear to me that initializing to "{ '\0' }" is a
great idea. I understand that the C standard says that an
initialization to {0} zeroes the whole struct, but if you try to pass
some other char value, it actually fills everything else with zeroes
rather than the other char value. So hiding the 0 byte as a \0 char is
misleading.)
3b. Also, if you had zeroed targetpath at initialization time, there
would no longer be a need to print a zero byte after calling readlink(),
so you could have removed the "targetpath[rllen] = '\0';" line.
However as I said above, I'm not a fan of unnecessary initialization.
4. Using StringInfo in this function is pointless. You use that when
you're going to do a bunch of string manipulation ops, appending more
data after the first, or using sprintf() formatted strings and so on.
But here you return just one or two possible strings with no
construction involved. Might as well use standard pstrdup() as needed,
which keeps the code simple.
5. Single-statement blocks need no braces.
6. ereport() used to require an extra set of parenthesis, but no more.
Remove those.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Tiene valor aquel que admite que es un cobarde" (Fernandel)
Attachments:
review.patch.txttext/plain; charset=utf-8Download
commit 08f168ab38d635f9e33480b506f3a0d667f80244
Author: Ãlvaro Herrera <alvherre@kurilemu.de> [Ãlvaro Herrera <alvherre@kurilemu.de>]
AuthorDate: Tue Nov 11 15:48:45 2025 +0100
CommitDate: Tue Nov 11 15:48:45 2025 +0100
[]review changes
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index c00888456c8..d23aa7ae274 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -3,7 +3,7 @@
* pg_tablespace.c
* routines to support tablespaces
*
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 2011-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
@@ -14,6 +14,7 @@
*/
#include "postgres.h"
+#include <unistd.h>
#include <sys/stat.h>
#include "catalog/pg_tablespace.h"
@@ -22,25 +23,21 @@
/*
- * get_tablespace_loc_string - Get a tablespace's location as a C-string for a
- * tablespace from its OID.
+ * get_tablespace_location
+ * Get a tablespace's location as a C-string, by its OID
*/
char *
-get_tablespace_loc_string(Oid tablespaceOid)
+get_tablespace_location(Oid tablespaceOid)
{
- char sourcepath[MAXPGPATH] = {'\0'};
- char targetpath[MAXPGPATH] = {'\0'};
+ char sourcepath[MAXPGPATH];
+ char targetpath[MAXPGPATH];
int rllen;
struct stat st;
- StringInfoData buf;
-
- initStringInfo(&buf);
- appendStringInfoString(&buf, "");
/*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
+ * It's useful to apply this to pg_class.reltablespace, wherein zero means
+ * "the database's default tablespace". So, rather than throwing an error
+ * for zero, we choose to assume that's what is meant.
*/
if (tablespaceOid == InvalidOid)
tablespaceOid = MyDatabaseTableSpace;
@@ -50,7 +47,7 @@ get_tablespace_loc_string(Oid tablespaceOid)
*/
if (tablespaceOid == DEFAULTTABLESPACE_OID ||
tablespaceOid == GLOBALTABLESPACE_OID)
- return buf.data;
+ return pstrdup("");
/*
* Find the location of the tablespace by reading the symbolic link that
@@ -65,36 +62,29 @@ get_tablespace_loc_string(Oid tablespaceOid)
* found, a relative path to the data directory is returned.
*/
if (lstat(sourcepath, &st) < 0)
- {
ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
+ errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath));
if (!S_ISLNK(st.st_mode))
- {
- appendStringInfoString(&buf, sourcepath);
- return buf.data;
- }
+ return pstrdup(sourcepath);
/*
- * In presence of a link or a junction point, return the path pointing to.
+ * In presence of a link or a junction point, return the path pointed to.
*/
rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
if (rllen < 0)
ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
+ errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath));
if (rllen >= sizeof(targetpath))
ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
+ errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath));
targetpath[rllen] = '\0';
- appendStringInfoString(&buf, targetpath);
-
- return buf.data;
+ return pstrdup(targetpath);
}
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index cb99d7435eb..a365c432d34 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -318,7 +318,7 @@ pg_tablespace_location(PG_FUNCTION_ARGS)
char *tablespaceLoc;
/* Get LOCATION string from its OID */
- tablespaceLoc = get_tablespace_loc_string(tablespaceOid);
+ tablespaceLoc = get_tablespace_location(tablespaceOid);
PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index f065cff9ddc..7816d779d8c 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -54,6 +54,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
-extern char *get_tablespace_loc_string(Oid tablespaceOid);
+extern char *get_tablespace_location(Oid tablespaceOid);
#endif /* PG_TABLESPACE_H */
On Tue, Nov 11, 2025 at 9:16 AM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-10, Nishant Sharma wrote:
PFA, v10 patch set.
I propose the following changes for 0001, in patch hunk ordering.
1. pg_tablespace_location was introduced in 2011 (commit 16d8e594acd9),
so claim copyright starting at that point.2. readlink(2) claims, at least in my system, to need <unistd.h>. Add
that.3. get_tablespace_loc_string() is such an ugly name. Why not
get_tablespace_location()?3. The initialization of sourcepath and targetpath are mostly pointless
(see below), so I'd leave it out.3a. (Also, it's not clear to me that initializing to "{ '\0' }" is a
great idea. I understand that the C standard says that an
initialization to {0} zeroes the whole struct, but if you try to pass
some other char value, it actually fills everything else with zeroes
rather than the other char value. So hiding the 0 byte as a \0 char is
misleading.)3b. Also, if you had zeroed targetpath at initialization time, there
would no longer be a need to print a zero byte after calling readlink(),
so you could have removed the "targetpath[rllen] = '\0';" line.
However as I said above, I'm not a fan of unnecessary initialization.4. Using StringInfo in this function is pointless. You use that when
you're going to do a bunch of string manipulation ops, appending more
data after the first, or using sprintf() formatted strings and so on.
But here you return just one or two possible strings with no
construction involved. Might as well use standard pstrdup() as needed,
which keeps the code simple.5. Single-statement blocks need no braces.
6. ereport() used to require an extra set of parenthesis, but no more.
Remove those.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
"Tiene valor aquel que admite que es un cobarde" (Fernandel)
Thanks, Álvaro, for your continued help with this.
I have attached v11 patches that use all of the fixes from your
review.patch.txt.
I have built and tested this using both make/autotools and meson, and I had
Nishant (thanks, Nishant!) look at these before posting, so hopefully
everything will build correctly.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v11-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v11-0002-Adds-pg_get_tablespace_ddl-function.patchDownload
From efe8c1ab7273a38380c45a5260f7c663b10e6a25 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Tue, 11 Nov 2025 18:34:12 -0600
Subject: [PATCH v11 2/2] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/catalog/pg_tablespace.c | 119 +++++++++++++++++++++++
src/backend/utils/adt/ruleutils.c | 37 +++++++
src/include/catalog/pg_proc.dat | 6 ++
src/include/catalog/pg_tablespace.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++++
7 files changed, 324 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..64c56e0a469 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index c205b883502..964184aefc5 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -17,9 +17,13 @@
#include <unistd.h>
#include <sys/stat.h>
+#include "access/htup_details.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
/*
@@ -88,3 +92,118 @@ get_tablespace_location(Oid tablespaceOid)
return pstrdup(targetpath);
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement as a
+ * C-string for a tablespace from its OID.
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_location(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..dc5365fbeae 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13743,3 +13744,39 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 7816d779d8c..b70035832df 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -55,5 +55,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
extern char *get_tablespace_location(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* PG_TABLESPACE_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
v11-0001-Supporting-changes-for-pg_get_tablespace_ddl-fun.patchtext/x-patch; charset=UTF-8; name=v11-0001-Supporting-changes-for-pg_get_tablespace_ddl-fun.patchDownload
From 40ee6ffe76b987c52a2affda41ba73f6217c0355 Mon Sep 17 00:00:00 2001
From: Nishant Sharma <nishant.sharma@enterprisedb.com>
Date: Wed, 12 Nov 2025 14:24:49 +0530
Subject: [PATCH v11 1/2] Supporting changes for pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Moving core logic of pg_tablespace_location() to new generic function
get_tablespace_loc_string()
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
src/backend/catalog/Makefile | 1 +
src/backend/catalog/meson.build | 1 +
src/backend/catalog/pg_tablespace.c | 90 +++++++++++++++++++++++++++++
src/backend/utils/adt/misc.c | 62 ++------------------
src/include/catalog/pg_tablespace.h | 2 +
5 files changed, 98 insertions(+), 58 deletions(-)
create mode 100644 src/backend/catalog/pg_tablespace.c
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..8e40e1b8189 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -44,6 +44,7 @@ OBJS = \
pg_range.o \
pg_shdepend.o \
pg_subscription.o \
+ pg_tablespace.o \
pg_type.o \
storage.o \
toasting.o
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 1958ea9238a..58674ffeee6 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -31,6 +31,7 @@ backend_sources += files(
'pg_range.c',
'pg_shdepend.c',
'pg_subscription.c',
+ 'pg_tablespace.c',
'pg_type.c',
'storage.c',
'toasting.c',
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
new file mode 100644
index 00000000000..c205b883502
--- /dev/null
+++ b/src/backend/catalog/pg_tablespace.c
@@ -0,0 +1,90 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_tablespace.c
+ * routines to support tablespaces
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_tablespace.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "catalog/pg_tablespace.h"
+#include "commands/tablespace.h"
+#include "miscadmin.h"
+
+
+/*
+ * get_tablespace_location
+ * Get a tablespace's location as a C-string, by its OID
+ */
+char *
+get_tablespace_location(Oid tablespaceOid)
+{
+ char sourcepath[MAXPGPATH];
+ char targetpath[MAXPGPATH];
+ int rllen;
+ struct stat st;
+
+ /*
+ * It's useful to apply this to pg_class.reltablespace, wherein zero means
+ * "the database's default tablespace". So, rather than throwing an error
+ * for zero, we choose to assume that's what is meant.
+ */
+ if (tablespaceOid == InvalidOid)
+ tablespaceOid = MyDatabaseTableSpace;
+
+ /*
+ * Return empty string for the cluster's default tablespaces
+ */
+ if (tablespaceOid == DEFAULTTABLESPACE_OID ||
+ tablespaceOid == GLOBALTABLESPACE_OID)
+ return pstrdup("");
+
+ /*
+ * Find the location of the tablespace by reading the symbolic link that
+ * is in pg_tblspc/<oid>.
+ */
+ snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
+
+ /*
+ * Before reading the link, check if the source path is a link or a
+ * junction point. Note that a directory is possible for a tablespace
+ * created with allow_in_place_tablespaces enabled. If a directory is
+ * found, a relative path to the data directory is returned.
+ */
+ if (lstat(sourcepath, &st) < 0)
+ ereport(ERROR,
+ errcode_for_file_access(),
+ errmsg("could not stat file \"%s\": %m",
+ sourcepath));
+
+ if (!S_ISLNK(st.st_mode))
+ return pstrdup(sourcepath);
+
+ /*
+ * In presence of a link or a junction point, return the path pointed to.
+ */
+ rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
+ if (rllen < 0)
+ ereport(ERROR,
+ errcode_for_file_access(),
+ errmsg("could not read symbolic link \"%s\": %m",
+ sourcepath));
+ if (rllen >= sizeof(targetpath))
+ ereport(ERROR,
+ errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("symbolic link \"%s\" target is too long",
+ sourcepath));
+ targetpath[rllen] = '\0';
+
+ return pstrdup(targetpath);
+}
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index fa1cb675027..a365c432d34 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -315,66 +315,12 @@ Datum
pg_tablespace_location(PG_FUNCTION_ARGS)
{
Oid tablespaceOid = PG_GETARG_OID(0);
- char sourcepath[MAXPGPATH];
- char targetpath[MAXPGPATH];
- int rllen;
- struct stat st;
+ char *tablespaceLoc;
- /*
- * It's useful to apply this function to pg_class.reltablespace, wherein
- * zero means "the database's default tablespace". So, rather than
- * throwing an error for zero, we choose to assume that's what is meant.
- */
- if (tablespaceOid == InvalidOid)
- tablespaceOid = MyDatabaseTableSpace;
-
- /*
- * Return empty string for the cluster's default tablespaces
- */
- if (tablespaceOid == DEFAULTTABLESPACE_OID ||
- tablespaceOid == GLOBALTABLESPACE_OID)
- PG_RETURN_TEXT_P(cstring_to_text(""));
-
- /*
- * Find the location of the tablespace by reading the symbolic link that
- * is in pg_tblspc/<oid>.
- */
- snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid);
-
- /*
- * Before reading the link, check if the source path is a link or a
- * junction point. Note that a directory is possible for a tablespace
- * created with allow_in_place_tablespaces enabled. If a directory is
- * found, a relative path to the data directory is returned.
- */
- if (lstat(sourcepath, &st) < 0)
- {
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not stat file \"%s\": %m",
- sourcepath)));
- }
-
- if (!S_ISLNK(st.st_mode))
- PG_RETURN_TEXT_P(cstring_to_text(sourcepath));
-
- /*
- * In presence of a link or a junction point, return the path pointing to.
- */
- rllen = readlink(sourcepath, targetpath, sizeof(targetpath));
- if (rllen < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not read symbolic link \"%s\": %m",
- sourcepath)));
- if (rllen >= sizeof(targetpath))
- ereport(ERROR,
- (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
- errmsg("symbolic link \"%s\" target is too long",
- sourcepath)));
- targetpath[rllen] = '\0';
+ /* Get LOCATION string from its OID */
+ tablespaceLoc = get_tablespace_location(tablespaceOid);
- PG_RETURN_TEXT_P(cstring_to_text(targetpath));
+ PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc));
}
/*
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 5293488c630..7816d779d8c 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -54,4 +54,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
+extern char *get_tablespace_location(Oid tablespaceOid);
+
#endif /* PG_TABLESPACE_H */
--
2.47.3
On 2025-Nov-12, Manni Wood wrote:
Thanks, Álvaro, for your continued help with this.
I have attached v11 patches that use all of the fixes from your
review.patch.txt.
OK, thanks, I pushed 0001 now.
I think you could claim that some routines currently in
src/backend/commands/tablespace.c logically belong in the new file, but
unless you want to take on the task of moving a lot of other routines
under commands/ to their respective catalog/ file, then I think it's
more or less fine as is.
To be clear, I do not intend to do anything with your 0002 patch [for
now]. I'm going to let Andrew take these DDL-producing functions in his
hands. Here I'm just posting your 0002 again, to make the cfbot happy.
Thanks
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Nunca se desea ardientemente lo que solo se desea por razón" (F. Alexandre)
Attachments:
v12-0002-Adds-pg_get_tablespace_ddl-function.patchtext/x-diff; charset=utf-8Download
From 07247f97726ddaf05505e58a9d46550adf9a2eee Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Tue, 11 Nov 2025 18:34:12 -0600
Subject: [PATCH v12] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Author: Manni Wood <manni.wood@enterprisedb.com>
Author: Nishant Sharma <nishant.sharma@enterprisedb.com>
Reviewed-by: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewed-by: Ian Lawrence Barwick <barwick@gmail.com>
Reviewed-by: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Ãlvaro Herrera <alvherre@kurilemu.de>
Discussion: https://postgr.es/m/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/catalog/pg_tablespace.c | 119 +++++++++++++++++++++++
src/backend/utils/adt/ruleutils.c | 37 +++++++
src/include/catalog/pg_proc.dat | 6 ++
src/include/catalog/pg_tablespace.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++++
7 files changed, 324 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..64c56e0a469 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index 6aca24c231e..cb088851178 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -17,9 +17,13 @@
#include <unistd.h>
#include <sys/stat.h>
+#include "access/htup_details.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
/*
@@ -88,3 +92,118 @@ get_tablespace_location(Oid tablespaceOid)
return pstrdup(targetpath);
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement as a
+ * C-string for a tablespace from its OID.
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %d does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_location(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", ");
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..dc5365fbeae 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13743,3 +13744,39 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 7816d779d8c..b70035832df 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -55,5 +55,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
extern char *get_tablespace_location(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* PG_TABLESPACE_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.47.3
On Wed, Nov 12, 2025 at 10:15 AM Álvaro Herrera <alvherre@kurilemu.de>
wrote:
On 2025-Nov-12, Manni Wood wrote:
Thanks, Álvaro, for your continued help with this.
I have attached v11 patches that use all of the fixes from your
review.patch.txt.OK, thanks, I pushed 0001 now.
I think you could claim that some routines currently in
src/backend/commands/tablespace.c logically belong in the new file, but
unless you want to take on the task of moving a lot of other routines
under commands/ to their respective catalog/ file, then I think it's
more or less fine as is.To be clear, I do not intend to do anything with your 0002 patch [for
now]. I'm going to let Andrew take these DDL-producing functions in his
hands. Here I'm just posting your 0002 again, to make the cfbot happy.Thanks
--
Álvaro Herrera 48°01'N 7°57'E —
https://www.EnterpriseDB.com/
"Nunca se desea ardientemente lo que solo se desea por razón" (F.
Alexandre)
OK, thanks very much, Álvaro.
If you are OK with the current state of the patch, then I am happy to not
move any more functions into their respective catalog/ files. My co-author,
Nishant, should feel free to offer his opinion here too.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Hi Manni,
I just reviewed and tested the patch, just got a few small comments:
On Nov 13, 2025, at 00:15, Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-12, Manni Wood wrote:
Thanks, Álvaro, for your continued help with this.
I have attached v11 patches that use all of the fixes from your
review.patch.txt.OK, thanks, I pushed 0001 now.
I think you could claim that some routines currently in
src/backend/commands/tablespace.c logically belong in the new file, but
unless you want to take on the task of moving a lot of other routines
under commands/ to their respective catalog/ file, then I think it's
more or less fine as is.To be clear, I do not intend to do anything with your 0002 patch [for
now]. I'm going to let Andrew take these DDL-producing functions in his
hands. Here I'm just posting your 0002 again, to make the cfbot happy.Thanks
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Nunca se desea ardientemente lo que solo se desea por razón" (F. Alexandre)
<v12-0002-Adds-pg_get_tablespace_ddl-function.patch>
1.
```
+ errmsg("tablespace with oid %d does not exist",
```
Existing code all use “%u” to format oid. You may search for “oid %” to see that.
2.
```
+ /* Add the options in WITH clause */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ appendStringInfoString(&buf, ", “);
```
This two statements can be combined into one:
appendStringInfoString(&buf, “%s, “, TextDatumGetCString(optdatums[i]));
3
```
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION '%s'", path);
+ }
```
Instead of hardcoding single-quotes, we can use quote_literal_cstr().
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On Thu, Nov 13, 2025 at 12:07 AM Chao Li <li.evan.chao@gmail.com> wrote:
Hi Manni,
I just reviewed and tested the patch, just got a few small comments:
On Nov 13, 2025, at 00:15, Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-12, Manni Wood wrote:
Thanks, Álvaro, for your continued help with this.
I have attached v11 patches that use all of the fixes from your
review.patch.txt.OK, thanks, I pushed 0001 now.
I think you could claim that some routines currently in
src/backend/commands/tablespace.c logically belong in the new file, but
unless you want to take on the task of moving a lot of other routines
under commands/ to their respective catalog/ file, then I think it's
more or less fine as is.To be clear, I do not intend to do anything with your 0002 patch [for
now]. I'm going to let Andrew take these DDL-producing functions in his
hands. Here I'm just posting your 0002 again, to make the cfbot happy.Thanks
--
Álvaro Herrera 48°01'N 7°57'E —"Nunca se desea ardientemente lo que solo se desea por razón" (F.
Alexandre)
<v12-0002-Adds-pg_get_tablespace_ddl-function.patch>
1.
```
+ errmsg("tablespace with oid %d does not
exist",
```Existing code all use “%u” to format oid. You may search for “oid %” to
see that.2. ``` + /* Add the options in WITH clause */ + appendStringInfoString(&buf, TextDatumGetCString(optdatums[i])); + appendStringInfoString(&buf, ", “); ```This two statements can be combined into one:
appendStringInfoString(&buf, “%s, “, TextDatumGetCString(optdatums[i]));
3 ``` + if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0) + appendStringInfoString(&buf, " LOCATION ''"); + else + appendStringInfo(&buf, " LOCATION '%s'", path); + } ```Instead of hardcoding single-quotes, we can use quote_literal_cstr().
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Hello, Chao Li!
Thank you for the improvements to my patch, helping it follow more of
Postgres's coding conventions. Much appreciated.
I have attached v13 of the patch. Now that Álvaro has merged the contents
of the 0001 patch, I assume this v13 patch can be 0001 and not 0002.
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v13-0001-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v13-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From 04d4e447cfa2bcd8124f3b44fc4ea2ba6a813455 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Thu, 13 Nov 2025 10:10:03 -0600
Subject: [PATCH v13] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 +++++++++
src/backend/catalog/pg_tablespace.c | 118 +++++++++++++++++++++++
src/backend/utils/adt/ruleutils.c | 37 +++++++
src/include/catalog/pg_proc.dat | 6 ++
src/include/catalog/pg_tablespace.h | 1 +
src/test/regress/expected/tablespace.out | 69 +++++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++++
7 files changed, 323 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..64c56e0a469 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index 6aca24c231e..8f04ef7a6b2 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -17,9 +17,13 @@
#include <unistd.h>
#include <sys/stat.h>
+#include "access/htup_details.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
/*
@@ -88,3 +92,117 @@ get_tablespace_location(Oid tablespaceOid)
return pstrdup(targetpath);
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement as a
+ * C-string for a tablespace from its OID.
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %u does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_location(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION %s", quote_literal_cstr(path));
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ ArrayType *optarray;
+ Datum *optdatums;
+ int optcount;
+ int i;
+
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfo(&buf, "%s, ", TextDatumGetCString(optdatums[i]));
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
+ /* Closing WITH clause */
+ appendStringInfoChar(&buf, ')');
+ /* Cleanup the datums found */
+ pfree(optdatums);
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..dc5365fbeae 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -13743,3 +13744,39 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..0fcd0f4fa8f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 7816d779d8c..b70035832df 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -55,5 +55,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
extern char *get_tablespace_location(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* PG_TABLESPACE_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..6b77e3323d1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost=3.0);
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost=1.5, random_page_cost=1.1234567890, effective_io_concurrency=17, maintenance_io_concurrency=18);
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6a49100ca14 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = 3.0); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = 1.5, random_page_cost = 1.1234567890, effective_io_concurrency = 17, maintenance_io_concurrency = 18); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.51.2
Hi Manni,
I just reviewed v13 again, and still got a couple of comments:
On Nov 18, 2025, at 22:15, Manni Wood <manni.wood@enterprisedb.com> wrote:
<v13-0001-Adds-pg_get_tablespace_ddl-function.patch>
1. Do we need to perform some privilege check? I just did a test:
```
evantest=> \c
You are now connected to database "evantest" as user "evan".
evantest=> select pg_get_tablespace_ddl('pg_default');
pg_get_tablespace_ddl
-------------------------------------------
CREATE TABLESPACE pg_default OWNER chaol;
(1 row)
```
Where “evan” is a new user without grant any persuasion to it, but it can view the system default tablespace’s DDL. I don’t think that’s expected.
2.
```
+ optarray = DatumGetArrayTypeP(datum);
+
+ deconstruct_array_builtin(optarray, TEXTOID,
+ &optdatums, NULL, &optcount);
+
+ Assert(optcount);
+
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ for (i = 0; i < (optcount - 1); i++) /* Skipping last option */
+ {
+ /* Add the options in WITH clause */
+ appendStringInfo(&buf, "%s, ", TextDatumGetCString(optdatums[i]));
+ }
+
+ /* Adding the last remaining option */
+ appendStringInfoString(&buf, TextDatumGetCString(optdatums[i]));
```
This block of code is a duplicate of get_reloptions() defined in ruleutils.c, and get_reloptions() performs more checks. So I think build_tablespace_ddl_string() should be defined in ruleutils.c and reuse the existing helper function.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Hi Chao
On 19/11/2025 04:13, Chao Li wrote:
1. Do we need to perform some privilege check? I just did a test:
```
evantest=> \c
You are now connected to database "evantest" as user "evan".
evantest=> select pg_get_tablespace_ddl('pg_default');
pg_get_tablespace_ddl
-------------------------------------------
CREATE TABLESPACE pg_default OWNER chaol;
(1 row)
```Where “evan” is a new user without grant any persuasion to it, but it can view the system default tablespace’s DDL. I don’t think that’s expected.
It is expected. \db behaves similarly:
CREATE TABLESPACE ts LOCATION '/tmp/ts';
CREATE TABLESPACE
postgres=# CREATE USER foo;
CREATE ROLE
postgres=# SET ROLE foo;
SET
postgres=> \db ts
List of tablespaces
Name | Owner | Location
------+-------+----------
ts | jim | /tmp/ts
(1 row)
IIUC the user foo is just reading the catalog entry of the new
tablespace, which is fine. Of course, accessing the tablespace itself is
not allowed. See \db+ (calculates the tablespace's size)
postgres=> \db+ ts
ERROR: permission denied for tablespace ts
Best, Jim
On Wed, Nov 19, 2025 at 1:52 AM Jim Jones <jim.jones@uni-muenster.de> wrote:
Hi Chao
On 19/11/2025 04:13, Chao Li wrote:
1. Do we need to perform some privilege check? I just did a test:
```
evantest=> \c
You are now connected to database "evantest" as user "evan".
evantest=> select pg_get_tablespace_ddl('pg_default');
pg_get_tablespace_ddl
-------------------------------------------
CREATE TABLESPACE pg_default OWNER chaol;
(1 row)
```Where “evan” is a new user without grant any persuasion to it, but it
can view the system default tablespace’s DDL. I don’t think that’s expected.
It is expected. \db behaves similarly:
CREATE TABLESPACE ts LOCATION '/tmp/ts';
CREATE TABLESPACEpostgres=# CREATE USER foo;
CREATE ROLEpostgres=# SET ROLE foo;
SETpostgres=> \db ts
List of tablespaces
Name | Owner | Location
------+-------+----------
ts | jim | /tmp/ts
(1 row)IIUC the user foo is just reading the catalog entry of the new
tablespace, which is fine. Of course, accessing the tablespace itself is
not allowed. See \db+ (calculates the tablespace's size)postgres=> \db+ ts
ERROR: permission denied for tablespace tsBest, Jim
Hello, Chao.
Thanks as always for your ongoing help with improving this feature.
Instead of moving build_tablespace_ddl_string out of pg_tablespace.c, I
made get_reloptions visible outside of ruleutils.c.
Otherwise, I followed your advice on using get_reloptions to DRY up the
code.
Let me know what you think!
--
-- Manni Wood EDB: https://www.enterprisedb.com
Attachments:
v14-0001-Adds-pg_get_tablespace_ddl-function.patchtext/x-patch; charset=UTF-8; name=v14-0001-Adds-pg_get_tablespace_ddl-function.patchDownload
From d5dbad978a5f3d4d31cab1e256c2108ed1714740 Mon Sep 17 00:00:00 2001
From: Manni Wood <manni.wood@enterprisedb.com>
Date: Wed, 19 Nov 2025 16:01:36 -0600
Subject: [PATCH v14] Adds pg_get_tablespace_ddl function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently, there exist ways of "wholesale" dumping the DDL for an entire
database or even cluster; this function will ideally be part of a suite
of similar "retail" functions for dumping the DDL of various database
objects.
Authors: Manni Wood <manni.wood@enterprisedb.com> and Nishant Sharma
<nishant.sharma@enterprisedb.com>
Reviewers: Vaibhav Dalvi, Ian Barwick, Jim Jones, Álvaro Herrera
Discussion:
https://www.postgresql.org/message-id/flat/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 45 ++++++++++
src/backend/catalog/pg_tablespace.c | 100 +++++++++++++++++++++++
src/backend/utils/adt/ruleutils.c | 40 ++++++++-
src/include/catalog/pg_proc.dat | 6 ++
src/include/catalog/pg_tablespace.h | 1 +
src/include/utils/ruleutils.h | 2 +
src/test/regress/expected/tablespace.out | 69 ++++++++++++++++
src/test/regress/sql/tablespace.sql | 47 +++++++++++
8 files changed, 308 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..64c56e0a469 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Get Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_tablespace_ddl</primary>
+ </indexterm>
+ <function>pg_get_tablespace_ddl</function>
+ ( <parameter>tablespace</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a tablespace.
+ The result is a complete <command>CREATE TABLESPACE</command> statement.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c
index 6aca24c231e..efe6dbe51ca 100644
--- a/src/backend/catalog/pg_tablespace.c
+++ b/src/backend/catalog/pg_tablespace.c
@@ -17,9 +17,14 @@
#include <unistd.h>
#include <sys/stat.h>
+#include "access/htup_details.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/ruleutils.h"
+#include "utils/syscache.h"
/*
@@ -88,3 +93,98 @@ get_tablespace_location(Oid tablespaceOid)
return pstrdup(targetpath);
}
+
+/*
+ * build_tablespace_ddl_string - Build CREATE TABLESPACE statement as a
+ * C-string for a tablespace from its OID.
+ */
+char *
+build_tablespace_ddl_string(const Oid tspaceoid)
+{
+ char *path;
+ char *spcowner;
+ bool isNull;
+ Oid tspowneroid;
+ Datum datum;
+ HeapTuple tuple;
+ StringInfoData buf;
+ Form_pg_tablespace tspForm;
+
+ /* Look up the tablespace in pg_tablespace */
+ tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tspaceoid));
+
+ /* Confirm if tablespace OID was valid */
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with oid %u does not exist",
+ tspaceoid)));
+
+ /* Get tablespace's details from its tuple */
+ tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
+
+ initStringInfo(&buf);
+
+ /* Start building the CREATE TABLESPACE statement */
+ appendStringInfo(&buf, "CREATE TABLESPACE %s",
+ quote_identifier(NameStr(tspForm->spcname)));
+
+ /* Get the OID of the owner of the tablespace name */
+ tspowneroid = tspForm->spcowner;
+
+ /* Add OWNER clause, if the owner is not the current user */
+ if (GetUserId() != tspowneroid)
+ {
+ /* Get the owner name */
+ spcowner = GetUserNameFromId(tspowneroid, false);
+
+ appendStringInfo(&buf, " OWNER %s",
+ quote_identifier(spcowner));
+ pfree(spcowner);
+ }
+
+ /* Find tablespace directory path */
+ path = get_tablespace_location(tspaceoid);
+
+ /* Add directory LOCATION (path), if it exists */
+ if (path[0] != '\0')
+ {
+ /*
+ * Special case: if the tablespace was created with GUC
+ * "allow_in_place_tablespaces = true" and "LOCATION ''", path will
+ * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
+ * user originally specified.
+ */
+ if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
+ appendStringInfoString(&buf, " LOCATION ''");
+ else
+ appendStringInfo(&buf, " LOCATION %s", quote_literal_cstr(path));
+ }
+ /* Done with path */
+ pfree(path);
+
+ /* Get tablespace's options datum from the tuple */
+ datum = SysCacheGetAttr(TABLESPACEOID,
+ tuple,
+ Anum_pg_tablespace_spcoptions,
+ &isNull);
+
+ if (!isNull)
+ {
+ /* Start WITH clause */
+ appendStringInfoString(&buf, " WITH (");
+
+ /* Append reloptions to buf */
+ get_reloptions(&buf, datum);
+
+ /* Close WITH clause */
+ appendStringInfoChar(&buf, ')');
+ }
+
+ ReleaseSysCache(tuple);
+
+ /* Finally add semicolon to the statement */
+ appendStringInfoChar(&buf, ';');
+
+ return buf.data;
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..1c9ef0a597e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,7 @@
#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
@@ -536,7 +537,6 @@ static void add_cast_to(StringInfo buf, Oid typid);
static char *generate_qualified_type_name(Oid typid);
static text *string_to_text(char *str);
static char *flatten_reloptions(Oid relid);
-static void get_reloptions(StringInfo buf, Datum reloptions);
static void get_json_path_spec(Node *path_spec, deparse_context *context,
bool showimplicit);
static void get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan,
@@ -13619,7 +13619,7 @@ string_to_text(char *str)
/*
* Generate a C string representing a relation options from text[] datum.
*/
-static void
+void
get_reloptions(StringInfo buf, Datum reloptions)
{
Datum *options;
@@ -13743,3 +13743,39 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_tablespace_ddl_name - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes name as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name tspname = PG_GETARG_NAME(0);
+ Oid tspaceoid;
+ char *ddl_stmt;
+
+ /* Get the OID of the tablespace from its name */
+ tspaceoid = get_tablespace_oid(NameStr(*tspname), false);
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_tablespace_ddl_oid - Get CREATE TABLESPACE statement for a
+ * tablespace. This takes oid as parameter for pg_get_tablespace_ddl().
+ */
+Datum
+pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid tspaceoid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE TABLESPACE DDL statement from its OID */
+ ddl_stmt = build_tablespace_ddl_string(tspaceoid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index aaadfd8c748..77968d1b13f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,12 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8758', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'name', prosrc => 'pg_get_tablespace_ddl_name' },
+{ oid => '8759', descr => 'get CREATE statement for tablespace',
+ proname => 'pg_get_tablespace_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index 7816d779d8c..b70035832df 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -55,5 +55,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p
MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4);
extern char *get_tablespace_location(Oid tablespaceOid);
+extern char *build_tablespace_ddl_string(const Oid tspaceoid);
#endif /* PG_TABLESPACE_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..0a7d30de85a 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -54,4 +54,6 @@ extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern void get_reloptions(StringInfo buf, Datum reloptions);
+
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index a90e39e5738..aa8ec8da506 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -971,3 +971,72 @@ drop cascades to materialized view testschema.amv
drop cascades to table testschema.tablespace_acl
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+SET allow_in_place_tablespaces = true;
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+ pg_get_tablespace_ddl
+----------------------------------------------------
+ CREATE TABLESPACE regress_noopt_tblsp LOCATION '';
+(1 row)
+
+DROP TABLESPACE regress_noopt_tblsp;
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+ pg_get_tablespace_ddl
+-------------------------------------------------
+ CREATE TABLESPACE "regress_ tblsp" LOCATION '';
+(1 row)
+
+DROP TABLESPACE "regress_ tblsp";
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = '3.0'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+ pg_get_tablespace_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost='3.0');
+(1 row)
+
+DROP TABLESPACE regress_oneopt_tblsp;
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = '3.0'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+ pg_get_tablespace_ddl
+-----------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost='3.0');
+(1 row)
+
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', effective_io_concurrency = '17', maintenance_io_concurrency = '18'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ pg_get_tablespace_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18');
+(1 row)
+
+DROP TABLESPACE regress_allopt_tblsp;
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+ERROR: tablespace "regress_allopt_tblsp" does not exist
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+ERROR: tablespace "" does not exist
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
+ pg_get_tablespace_ddl
+-----------------------
+
+(1 row)
+
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index dfe3db096e2..6298ef1f37b 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -437,3 +437,50 @@ DROP SCHEMA testschema CASCADE;
DROP ROLE regress_tablespace_user1;
DROP ROLE regress_tablespace_user2;
+
+-- Test pg_get_tablespace_ddl() by creating tablespaces with various
+-- configurations and checking the DDL.
+
+SET allow_in_place_tablespaces = true;
+
+-- create a tablespace using no options
+CREATE TABLESPACE regress_noopt_tblsp LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_noopt_tblsp');
+DROP TABLESPACE regress_noopt_tblsp;
+
+-- create a tablespace with a space in the name
+CREATE TABLESPACE "regress_ tblsp" LOCATION ''; -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_ tblsp');
+DROP TABLESPACE "regress_ tblsp";
+
+-- create a tablespace using one option
+CREATE TABLESPACE regress_oneopt_tblsp LOCATION '' WITH (random_page_cost = '3.0'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_oneopt_tblsp');
+DROP TABLESPACE regress_oneopt_tblsp;
+
+-- create tablespace owned by a particular user
+CREATE USER regress_user;
+CREATE TABLESPACE regress_owner_tblsp OWNER regress_user LOCATION '' WITH (random_page_cost = '3.0'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_owner_tblsp');
+DROP TABLESPACE regress_owner_tblsp;
+DROP USER regress_user;
+
+-- create a tablespace using all the options
+CREATE TABLESPACE regress_allopt_tblsp LOCATION '' WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', effective_io_concurrency = '17', maintenance_io_concurrency = '18'); -- ok
+-- see that the tablespace ddl is correctly returned
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+DROP TABLESPACE regress_allopt_tblsp;
+
+-- see the pg_get_tablespace_ddl error for a dropped 'regress_allopt_tblsp'
+-- tablespace name
+SELECT pg_get_tablespace_ddl('regress_allopt_tblsp');
+
+-- see the pg_get_tablespace_ddl error for an empty input
+SELECT pg_get_tablespace_ddl('');
+
+-- see the pg_get_tablespace_ddl output for a NULL input
+SELECT pg_get_tablespace_ddl(NULL);
--
2.52.0
Thanks Manni for the v14 patch!
Thanks all for the review comments!
I think we are in position to move to 'Ready for Committer'
status.
CFBot is all green for v14:
https://commitfest.postgresql.org/patch/6175/
Hi Andrew,
We are moving this thread to 'Ready for Committer' for your
reviews and finalisation of the patch. Thanks!
Regards,
Nishant Sharma.
EDB, Pune.
https://www.enterprisedb.com/