[PATCH] Relocation of tablespaces in pg_basebackup

Started by Steeve Lennmarkabout 12 years ago26 messages
#1Steeve Lennmark
steevel@handeldsbanken.se
1 attachment(s)

Currently pg_basebackup is pretty invasive when using tablespaces, at
least using the plain format. This since it requires the tablespace to
be written to the same location as on the server beeing backed up. This
both breaks backing up locally using -Fp (since the tablespace would
be written to the same location) and requires the backup user to have
write permissions in locations it shouldn't need to have access to.

This patch adds the ability to relocate tablespaces by adding the
command line argument --tablespace (-T) which takes a required argument
in the format "oid:tablespacedir". After all tablespaces are fetched
this code updates the symlink to point to the new tablespace location.

I would have loved to be able to pass tablespacename:tablespacedir
though, but sadly I wasn't able to figure out how to retrieve that
information without creating another connection to the database.

This feature might be missing because of some other limitation I fail
to see, if so let me know. Please be gentle, this is my first patch ;-)

Attachments:

0001-SQL-assertions-prototype.patchapplication/octet-stream; name=0001-SQL-assertions-prototype.patchDownload
From 615f43ae291de545b350369c7b4f3397aff4d75e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Thu, 14 Nov 2013 22:13:56 -0500
Subject: [PATCH] SQL assertions prototype

---
 doc/src/sgml/ddl.sgml                      |  31 +++
 doc/src/sgml/ref/allfiles.sgml             |   3 +
 doc/src/sgml/ref/alter_assertion.sgml      | 138 ++++++++++++
 doc/src/sgml/ref/create_assertion.sgml     | 133 ++++++++++++
 doc/src/sgml/ref/drop_assertion.sgml       | 114 ++++++++++
 doc/src/sgml/reference.sgml                |   3 +
 src/backend/catalog/aclchk.c               |  31 +++
 src/backend/catalog/information_schema.sql |  15 +-
 src/backend/catalog/namespace.c            | 120 +++++++++++
 src/backend/catalog/objectaddress.c        |  10 +
 src/backend/catalog/pg_constraint.c        |  20 +-
 src/backend/commands/alter.c               |   6 +
 src/backend/commands/constraint.c          | 335 +++++++++++++++++++++++++++++
 src/backend/commands/event_trigger.c       |   2 +
 src/backend/nodes/copyfuncs.c              |  14 ++
 src/backend/nodes/equalfuncs.c             |  12 ++
 src/backend/parser/gram.y                  |  73 ++++---
 src/backend/parser/parse_agg.c             |   2 +
 src/backend/parser/parse_expr.c            |   2 +
 src/backend/tcop/utility.c                 |  20 ++
 src/bin/psql/command.c                     |   3 +
 src/bin/psql/describe.c                    |  91 ++++++++
 src/bin/psql/describe.h                    |   3 +
 src/bin/psql/help.c                        |   1 +
 src/bin/psql/tab-complete.c                |  35 ++-
 src/include/catalog/namespace.h            |   1 +
 src/include/catalog/pg_constraint.h        |   5 +-
 src/include/catalog/pg_proc.h              |   3 +
 src/include/commands/constraint.h          |  10 +
 src/include/nodes/nodes.h                  |   1 +
 src/include/nodes/parsenodes.h             |  12 ++
 src/include/parser/parse_node.h            |   1 +
 src/include/utils/acl.h                    |   2 +
 src/include/utils/builtins.h               |   1 +
 src/test/regress/expected/assertions.out   |  58 +++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/assertions.sql        |  31 +++
 38 files changed, 1303 insertions(+), 42 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_assertion.sgml
 create mode 100644 doc/src/sgml/ref/create_assertion.sgml
 create mode 100644 doc/src/sgml/ref/drop_assertion.sgml
 create mode 100644 src/include/commands/constraint.h
 create mode 100644 src/test/regress/expected/assertions.out
 create mode 100644 src/test/regress/sql/assertions.sql

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index bae2e97..e85e2ea 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -912,6 +912,37 @@ <title>Exclusion Constraints</title>
     of the type specified in the constraint declaration.
    </para>
   </sect2>
+
+  <sect2 id="ddl-constraints-assertions">
+   <title>Assertions</title>
+
+   <indexterm zone="ddl-constraints-assertions">
+    <primary>assertion</primary>
+   </indexterm>
+
+   <para>
+    An assertion is a constraint that is not part of a table
+    definition.  An assertion can define constraints that evaluate the
+    data across multiple rows of a table beyond what unique and
+    exclusion constraints can do, and assertions can look at the data
+    in multiple tables.
+   </para>
+
+   <para>
+    An assertion is a separate schema object and is created with the
+    command <command>CREATE ASSERTION</command>.  The constraint
+    expression is written as a <literal>CHECK</literal> clause like in
+    check constraints.  For instance, to ensure that there is always
+    at least one entry in the product table:
+<programlisting>
+CREATE ASSERTION products_not_empty CHECK ((SELECT count(*) FROM products) &gt; 0);
+</programlisting>
+    Assertions will often involve aggregate functions computed over
+    entire tables.  Note, however, that this kind of assertion can be
+    quite inefficient and should only be used on tables that are small
+    and change rarely.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="ddl-system-columns">
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 5846974..5ca4067 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -7,6 +7,7 @@
 <!-- SQL commands -->
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
+<!ENTITY alterAssertion     SYSTEM "alter_assertion.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
@@ -51,6 +52,7 @@
 <!ENTITY commitPrepared     SYSTEM "commit_prepared.sgml">
 <!ENTITY copyTable          SYSTEM "copy.sgml">
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
+<!ENTITY createAssertion    SYSTEM "create_assertion.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
@@ -91,6 +93,7 @@
 <!ENTITY discard            SYSTEM "discard.sgml">
 <!ENTITY do                 SYSTEM "do.sgml">
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
+<!ENTITY dropAssertion      SYSTEM "drop_assertion.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
diff --git a/doc/src/sgml/ref/alter_assertion.sgml b/doc/src/sgml/ref/alter_assertion.sgml
new file mode 100644
index 0000000..14fe015
--- /dev/null
+++ b/doc/src/sgml/ref/alter_assertion.sgml
@@ -0,0 +1,138 @@
+<!--
+doc/src/sgml/ref/alter_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="SQL-ALTERASSERTION">
+ <refmeta>
+  <refentrytitle>ALTER ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER ASSERTION</refname>
+  <refpurpose>change the definition of an assertion</refpurpose>
+ </refnamediv>
+
+ <indexterm zone="sql-alterassertion">
+  <primary>ALTER ASSERTION</primary>
+ </indexterm>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER ASSERTION <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> OWNER TO <replaceable>new_owner</replaceable>
+ALTER ASSERTION <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER ASSERTION</command> changes the definition of an
+   assertion.
+  </para>
+
+  <para>
+   You must own the assertion to use <command>ALTER ASSERTION</>.  To
+   change the schema of an assertion, you must also have
+   <literal>CREATE</literal> privilege on the new schema.  To alter
+   the owner, you must also be a direct or indirect member of the new
+   owning role, and that role must have <literal>CREATE</literal>
+   privilege on the assertion's schema.  (These restrictions enforce
+   that altering the owner doesn't do anything you couldn't do by
+   dropping and recreating the assertion.  However, a superuser can
+   alter ownership of any assertion anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the assertuib <literal>check_size</literal>
+   to <literal>check_count</literal>:
+<programlisting>
+ALTER ASSERTION check_size RENAME TO check_count;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the assertion <literal>check_size</literal>
+   to <literal>joe</literal>:
+<programlisting>
+ALTER ASSERTION check_size OWNER TO joe;
+</programlisting>
+  </para>
+
+  <para>
+   To move the assertion <literal>check_size</literal> into
+   schema <literal>myschema</literal>:
+<programlisting>
+ALTER ASSERTION check_size SET SCHEMA myschema;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER ASSERTION</command> statement in the SQL
+   standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createassertion"></member>
+   <member><xref linkend="sql-dropassertion"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_assertion.sgml b/doc/src/sgml/ref/create_assertion.sgml
new file mode 100644
index 0000000..8ae4ec8
--- /dev/null
+++ b/doc/src/sgml/ref/create_assertion.sgml
@@ -0,0 +1,133 @@
+<!--
+doc/src/sgml/ref/create_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="SQL-CREATEASSERTION">
+ <refmeta>
+  <refentrytitle>CREATE ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE ASSERTION</refname>
+  <refpurpose>define a new assertion</refpurpose>
+ </refnamediv>
+
+ <indexterm zone="sql-createassertion">
+  <primary>CREATE ASSERTION</primary>
+ </indexterm>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE ASSERTION <replaceable class="PARAMETER">name</replaceable> CHECK ( <replaceable class="PARAMETER">name</replaceable> ) [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> creates an assertion.  An
+   assertion is a check constraint that is independent of a table row
+   and a table.  It can therefore be used to enforce more complex
+   constraints across multiple table rows and across multiple tables.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="PARAMETER">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the assertion to
+      create.  Assertions use the same namespace as constraints on
+      tables.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CHECK ( <replaceable class="PARAMETER">expression</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      The <literal>CHECK</> clause specifies an expression producing a
+      Boolean result which the database must satisfy at all times for a
+      data change operation to succeed.  Expressions evaluating
+      to TRUE or UNKNOWN succeed.  Should the result of a data change
+      operation produce a FALSE result an error exception is
+      raised and the change is not made.
+     </para>
+
+     <para>
+      The check expression typically involves subselects in order to
+      read data from tables.  See the examples below.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>DEFERRABLE</literal></term>
+    <term><literal>NOT DEFERRABLE</literal></term>
+    <term><literal>INITIALLY IMMEDIATE</literal></term>
+    <term><literal>INITIALLY DEFERRED</literal></term>
+    <listitem>
+     <para>
+      These clauses control the deferrability of the constraint.  See
+      <xref linkend="sql-createtable"> for an explanation.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   More specialized constraint forms such as table check constraints,
+   foreign-key constraints, or exclusion constraints should be used
+   instead when applicable, because they will be more efficient.
+  </para>
+
+  <para>
+   Assertion checks are not specially optimized.  For example,
+   checking the row count of a large table in an assertion will be
+   just as slow as implementing the check manually.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   Check that the table <literal>table1</literal> has at most 30 rows:
+<programlisting>
+CREATE ASSERTION table1_max30 CHECK ((SELECT count(*) FROM table1) &lt;= 30);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE ASSERTION</command> conforms to the SQL standard.
+   The PostgreSQL implementation has certain restrictions on what
+   check expressions are allowed in assertions.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"></member>
+   <member><xref linkend="sql-dropassertion"></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/drop_assertion.sgml b/doc/src/sgml/ref/drop_assertion.sgml
new file mode 100644
index 0000000..9ecfc3e
--- /dev/null
+++ b/doc/src/sgml/ref/drop_assertion.sgml
@@ -0,0 +1,114 @@
+<!--
+doc/src/sgml/ref/drop_assertion.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="SQL-DROPASSERTION">
+ <refmeta>
+  <refentrytitle>DROP ASSERTION</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP ASSERTION</refname>
+  <refpurpose>remove an assertion</refpurpose>
+ </refnamediv>
+
+ <indexterm zone="sql-dropassertion">
+  <primary>DROP ASSERTION</primary>
+ </indexterm>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP ASSERTION [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP ASSERTION</command> removes an existing assertion. To
+   execute this command the current user must be the owner of the
+   assertion.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the assertion does not exist. A notice
+      is issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the assertion.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the assertion if any objects depend on it.  This
+      is the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To remove the assertion <literal>check_size</literal>:
+<programlisting>
+DROP ASSERTION check_size;
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   This command conforms to the SQL standard, except that the standard
+   only allows one assertion to be dropped per command, and apart from
+   the <literal>IF EXISTS</> option, which is
+   a <productname>PostgreSQL</> extension.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alterassertion"></member>
+   <member><xref linkend="sql-createassertion"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 14e217a..a749e74 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -35,6 +35,7 @@ <title>SQL Commands</title>
 
    &abort;
    &alterAggregate;
+   &alterAssertion;
    &alterCollation;
    &alterConversion;
    &alterDatabase;
@@ -79,6 +80,7 @@ <title>SQL Commands</title>
    &commitPrepared;
    &copyTable;
    &createAggregate;
+   &createAssertion;
    &createCast;
    &createCollation;
    &createConversion;
@@ -119,6 +121,7 @@ <title>SQL Commands</title>
    &discard;
    &do;
    &dropAggregate;
+   &dropAssertion;
    &dropCast;
    &dropCollation;
    &dropConversion;
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 06aa766..a371e73 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3291,6 +3291,8 @@ static const char *const no_priv_msg[MAX_ACL_KIND] =
 	gettext_noop("permission denied for operator family %s"),
 	/* ACL_KIND_COLLATION */
 	gettext_noop("permission denied for collation %s"),
+	/* ACL_KIND_CONSTRAINT */
+	gettext_noop("permission denied for constraint %s"),
 	/* ACL_KIND_CONVERSION */
 	gettext_noop("permission denied for conversion %s"),
 	/* ACL_KIND_TABLESPACE */
@@ -3337,6 +3339,8 @@ static const char *const not_owner_msg[MAX_ACL_KIND] =
 	gettext_noop("must be owner of operator family %s"),
 	/* ACL_KIND_COLLATION */
 	gettext_noop("must be owner of collation %s"),
+	/* ACL_KIND_CONSTRAINT */
+	gettext_noop("must be owner of constraint %s"),
 	/* ACL_KIND_CONVERSION */
 	gettext_noop("must be owner of conversion %s"),
 	/* ACL_KIND_TABLESPACE */
@@ -4983,6 +4987,33 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 }
 
 /*
+ * Ownership check for a constraint (specified by OID).
+ */
+bool
+pg_constraint_ownercheck(Oid constr_oid, Oid roleid)
+{
+	HeapTuple   tuple;
+	//Oid           ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constr_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("constraint with OID %u does not exist", constr_oid)));
+
+	//FIXME: ownerId = ((Form_pg_constraint) GETSTRUCT(tuple))->conowner;
+
+	ReleaseSysCache(tuple);
+
+	//return has_privs_of_role(roleid, ownerId);
+	return true; // for now
+}
+
+/*
  * Ownership check for a conversion (specified by OID).
  */
 bool
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index c5f7a8b..ba5bccb 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -268,7 +268,20 @@ CREATE VIEW administrable_role_authorizations AS
  * ASSERTIONS view
  */
 
--- feature not supported
+CREATE VIEW assertions AS
+    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
+           CAST(n.nspname AS sql_identifier) AS constraint_schema,
+           CAST(con.conname AS sql_identifier) AS constraint_name,
+           CAST(CASE WHEN condeferrable THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS is_deferrable,
+           CAST(CASE WHEN condeferred THEN 'YES' ELSE 'NO' END
+             AS yes_or_no) AS initially_deferred
+    FROM pg_namespace n, pg_constraint con
+    WHERE n.oid = con.connamespace
+          AND con.conrelid = 0 AND con.contypid = 0;
+          -- TODO: AND pg_has_role(con.conowner, 'USAGE');
+
+GRANT SELECT ON assertions TO PUBLIC;
 
 
 /*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 4434dd6..1bb5b24 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -22,9 +22,11 @@
 #include "access/htup_details.h"
 #include "access/xact.h"
 #include "catalog/dependency.h"
+#include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_conversion_fn.h"
 #include "catalog/pg_namespace.h"
@@ -49,11 +51,13 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/catcache.h"
+#include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
+#include "utils/tqual.h"
 
 
 /*
@@ -3256,6 +3260,122 @@ PopOverrideSearchPath(void)
 }
 
 
+static Oid
+get_assertion_oid_internal(Relation pg_constraint, char *assertion_name, Oid namespaceId, List *name)
+{
+	SysScanDesc scan;
+	ScanKeyData skey[4];
+	HeapTuple	tuple;
+	Oid			conOid = InvalidOid;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_conname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				PointerGetDatum(assertion_name));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_constraint_connamespace,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(namespaceId));
+
+	ScanKeyInit(&skey[2],
+				Anum_pg_constraint_conrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	ScanKeyInit(&skey[3],
+				Anum_pg_constraint_contypid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				InvalidOid);
+
+	scan = systable_beginscan(pg_constraint, InvalidOid, false,
+							  NULL, 4, skey);
+
+	/*
+	 * Fetch the constraint tuple from pg_constraint.  There may be
+	 * more than one match, because constraints are not required to
+	 * have unique names; if so, error out.
+	 */
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		if (OidIsValid(con->conrelid) || OidIsValid(con->contypid))
+			ereport(ERROR,
+					(errmsg("constraint \"%s\" is not an assertion",
+							NameListToString(name))));
+
+		if (strcmp(NameStr(con->conname), assertion_name) == 0)
+		{
+			if (OidIsValid(conOid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("there are multiple assertions named \"%s\"",
+								NameListToString(name))));
+			conOid = HeapTupleGetOid(tuple);
+		}
+	}
+
+	systable_endscan(scan);
+
+	return conOid;
+}
+
+/*
+ * get_assertion_oid
+ *		Find an assertion with the specified name.
+ *		Returns constraint's OID.
+ */
+Oid
+get_assertion_oid(List *name, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *assertion_name;
+	Oid			namespaceId;
+	Relation	pg_constraint;
+	Oid			conOid = InvalidOid;
+
+	DeconstructQualifiedName(name, &schemaname, &assertion_name);
+
+	pg_constraint = heap_open(ConstraintRelationId, AccessShareLock);
+
+	if (schemaname)
+	{
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+	}
+	else
+	{
+		ListCell   *l;
+
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			conOid = get_assertion_oid_internal(pg_constraint, assertion_name, namespaceId, name);
+
+			if (OidIsValid(conOid))
+				break;
+		}
+	}
+
+	heap_close(pg_constraint, AccessShareLock);
+
+	/* If no such constraint exists, complain */
+	if (!OidIsValid(conOid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("assertion \"%s\" does not exist",
+						NameListToString(name))));
+
+	return conOid;
+}
+
 /*
  * get_collation_oid - find a collation by possibly qualified name
  */
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 9011190..a14bd72 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -547,6 +547,11 @@ get_object_address(ObjectType objtype, List *objname, List *objargs,
 											missing_ok, -1);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_ASSERTION:
+				address.classId = ConstraintRelationId;
+				address.objectId = get_assertion_oid(objname, missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_COLLATION:
 				address.classId = CollationRelationId;
 				address.objectId = get_collation_oid(objname, missing_ok);
@@ -1164,6 +1169,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_NAMESPACE,
 							   NameListToString(objname));
 			break;
+		case OBJECT_ASSERTION:
+			if (!pg_constraint_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CONSTRAINT,
+							   NameListToString(objname));
+			break;
 		case OBJECT_COLLATION:
 			if (!pg_collation_ownercheck(address.objectId, roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_COLLATION,
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 5021420..5851626 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -428,6 +428,13 @@ ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 			found = true;
 			break;
 		}
+		else if (conCat == CONSTRAINT_ASSERTION
+				 && con->conrelid == InvalidOid
+				 && con->contypid == InvalidOid)
+		{
+			found = true;
+			break;
+		}
 	}
 
 	systable_endscan(conscan);
@@ -601,8 +608,7 @@ RemoveConstraintById(Oid conId)
 		 * but we have no such concept at the moment.
 		 */
 	}
-	else
-		elog(ERROR, "constraint %u is not of a known type", conId);
+	/* Else it's an assertion; nothing special for that. */
 
 	/* Fry the constraint itself */
 	simple_heap_delete(conDesc, &tup->t_self);
@@ -658,6 +664,16 @@ RenameConstraintById(Oid conId, const char *newname)
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for domain %s already exists",
 						newname, format_type_be(con->contypid))));
+	if (!OidIsValid(con->conrelid) &&
+		!OidIsValid(con->contypid) &&
+		ConstraintNameIsUsed(CONSTRAINT_ASSERTION,
+							 InvalidOid,
+							 con->connamespace,
+							 newname))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("assertion \"%s\" already exists",
+						newname)));
 
 	/* OK, do the rename --- tuple is a copy, so OK to scribble on it */
 	namestrcpy(&(con->conname), newname);
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index b62ec70..8efc6af 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -38,6 +38,7 @@
 #include "catalog/pg_ts_template.h"
 #include "commands/alter.h"
 #include "commands/collationcmds.h"
+#include "commands/constraint.h"
 #include "commands/conversioncmds.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
@@ -304,6 +305,9 @@ ExecRenameStmt(RenameStmt *stmt)
 {
 	switch (stmt->renameType)
 	{
+		case OBJECT_ASSERTION:
+			return RenameAssertion(stmt->object, stmt->newname);
+
 		case OBJECT_CONSTRAINT:
 			return RenameConstraint(stmt);
 
@@ -409,6 +413,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt)
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -703,6 +708,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index f2cdc27..9ef1f55 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -13,11 +13,32 @@
  */
 #include "postgres.h"
 
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
 #include "catalog/index.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_trigger.h"
+#include "commands/constraint.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/planner.h"
+#include "parser/analyze.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_node.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/tqual.h"
 
 
@@ -188,3 +209,317 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 
 	return PointerGetDatum(NULL);
 }
+
+
+Datum
+assertion_check(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const char *funcname = "assertion_check";
+	Oid			constraintOid;
+	HeapTuple	tup;
+	Datum		adatum;
+	bool		isNull;
+	char	   *query;
+	int			ret;
+
+	/*
+	 * Make sure this is being called as an AFTER STATEMENT trigger.	Note:
+	 * translatable error strings are shared with ri_triggers.c, so resist the
+	 * temptation to fold the function name into them.
+	 */
+	if (!CALLED_AS_TRIGGER(fcinfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+				 errmsg("function \"%s\" was not called by trigger manager",
+						funcname)));
+
+	if (!TRIGGER_FIRED_AFTER(trigdata->tg_event) ||
+		!TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
+		ereport(ERROR,
+				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+				 errmsg("function \"%s\" must be fired AFTER STATEMENT",
+						funcname)));
+
+	constraintOid = trigdata->tg_trigger->tgconstraint;
+	tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for constraint %u", constraintOid);
+
+	// XXX bogus
+	adatum = SysCacheGetAttr(CONSTROID, tup,
+							 Anum_pg_constraint_consrc, &isNull);
+	if (isNull)
+		elog(ERROR, "constraint %u has null consrc", constraintOid);
+
+	SPI_connect();
+	query = psprintf("SELECT %s", TextDatumGetCString(adatum));
+	ret = SPI_exec(query, 1);
+
+	if (ret > 0 && SPI_tuptable != NULL)
+	{
+		Datum val = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isNull);
+		if (!isNull && DatumGetBool(val) == false)
+			ereport(ERROR,
+					(errcode(ERRCODE_CHECK_VIOLATION),
+					 errmsg("assertion \"%s\" violated",
+							get_constraint_name(constraintOid))));
+	}
+	else
+		elog(ERROR, "unexpected SPI result");
+
+	SPI_finish();
+	ReleaseSysCache(tup);
+	return PointerGetDatum(NULL);
+}
+
+
+struct collect_used_tables_context
+{
+	List *rtable;
+	List *rels;
+};
+
+static bool
+collect_used_tables_walker(Node *node, struct collect_used_tables_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RangeTblRef))
+	{
+		RangeTblRef *ref;
+		Oid relid;
+
+		ref= (RangeTblRef *) node;
+		relid = getrelid(ref->rtindex, context->rtable);
+		context->rels = list_append_unique_oid(context->rels, relid);
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		Query *query;
+		List *old_rtable;
+		bool result;
+
+		query = (Query *) node;
+		old_rtable = context->rtable;
+		context->rtable = query->rtable;
+		result = query_tree_walker(query, collect_used_tables_walker, context, 0);
+		context->rtable = old_rtable;
+		return result;
+	}
+
+	return expression_tree_walker(node, collect_used_tables_walker, (void *) context);
+}
+
+void
+CreateAssertion(CreateAssertionStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *assertion_name;
+	AclResult	aclresult;
+	Node	   *expr;
+	ParseState *pstate;
+	char	   *ccsrc;
+	char	   *ccbin;
+	Oid			constrOid;
+	struct collect_used_tables_context context;
+	ListCell   *lc;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->assertion_name,
+													&assertion_name);
+
+	aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
+					   get_namespace_name(namespaceId));
+
+	// TODO: check constraint name
+
+	pstate = make_parsestate(NULL);
+	expr = transformExpr(pstate, stmt->constraint->raw_expr, EXPR_KIND_ASSERTION_CHECK);
+	expr = coerce_to_boolean(pstate, expr, "CHECK");
+
+	ccbin = nodeToString(expr);
+	ccsrc = deparse_expression(expr, NIL, false, false);
+
+	constrOid = CreateConstraintEntry(assertion_name,
+						  namespaceId,
+						  CONSTRAINT_CHECK, /* constraint type */
+						  stmt->constraint->deferrable,
+						  stmt->constraint->initdeferred,
+						  !stmt->constraint->skip_validation,
+						  InvalidOid, /* not a relation constraint */
+						  NULL,		  /* no keys */
+						  0,		  /* no keys */
+						  InvalidOid, /* not a domain constraint */
+						  InvalidOid, /* no associated index */
+						  InvalidOid, /* foreign key fields ... */
+						  NULL,
+						  NULL,
+						  NULL,
+						  NULL,
+						  0,
+						  ' ',
+						  ' ',
+						  ' ',
+						  NULL,	/* not an exclusion constraint */
+						  expr, /* tree form of check constraint */
+						  ccbin, /* binary form of check constraint */
+						  ccsrc, /* source form of check constraint */
+						  true, /* is local */
+						  0,   /* inhcount */
+						  false, /* noinherit XXX */
+						  false); /* is_internal */
+
+	context.rtable = NIL;
+	context.rels = NIL;
+	collect_used_tables_walker(expr, &context);
+
+	foreach (lc, context.rels)
+	{
+		Oid relid = lfirst_oid(lc);
+		CreateTrigStmt *trigger;
+		Relation rel;
+		ObjectAddress myself,
+			referenced;
+
+		rel = heap_open(relid, ShareLock); // XXX
+
+		trigger = makeNode(CreateTrigStmt);
+		trigger->trigname = "AssertionTrigger";
+		trigger->relation = makeRangeVar(get_namespace_name(namespaceId),
+										 pstrdup(RelationGetRelationName(rel)),
+										 -1);
+		trigger->funcname = SystemFuncName("assertion_check");
+		trigger->args = NIL;
+		trigger->row = false;
+		trigger->timing = TRIGGER_TYPE_AFTER;
+		trigger->events = TRIGGER_TYPE_INSERT | TRIGGER_TYPE_UPDATE | TRIGGER_TYPE_DELETE | TRIGGER_TYPE_TRUNCATE;
+		trigger->columns = NIL;
+		trigger->whenClause = NULL;
+		trigger->isconstraint = true;
+		trigger->deferrable = stmt->constraint->deferrable;
+		trigger->initdeferred = stmt->constraint->initdeferred;
+		trigger->constrrel = NULL;
+
+		CreateTrigger(trigger, NULL, constrOid, InvalidOid, true);
+
+		heap_close(rel, NoLock);
+
+		/*
+		 * Record a dependency between the constraint and the table.
+		 */
+
+		myself.classId = ConstraintRelationId;
+		myself.objectId = constrOid;
+		myself.objectSubId = 0;
+
+		referenced.classId = RelationRelationId;
+		referenced.objectId = relid;
+		referenced.objectSubId = 0;
+
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+}
+
+
+void
+DropAssertion(DropStmt *drop)
+{
+	ObjectAddresses *objects;
+	ListCell   *cell;
+
+	/*
+	 * First we identify all the assertions, then we delete them in a single
+	 * performMultipleDeletions() call.  This is to avoid unwanted DROP
+	 * RESTRICT errors if one of the assertions depends on another. (Not that
+	 * that is very likely, but we may as well do this consistently.)
+	 */
+	objects = new_object_addresses();
+
+	foreach(cell, drop->objects)
+	{
+		List	   *name = (List *) lfirst(cell);
+		Oid			assertionOid;
+		HeapTuple	tuple;
+		Form_pg_constraint con;
+		ObjectAddress object;
+
+		assertionOid = get_assertion_oid(name, drop->missing_ok);
+
+		if (!OidIsValid(assertionOid))
+		{
+			ereport(NOTICE,
+					(errmsg("assertion \"%s\" does not exist, skipping",
+							NameListToString(name))));
+			continue;
+		}
+
+		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(assertionOid));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for constraint %u",
+				 assertionOid);
+		con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		/* Permission check: must own constraint or its namespace */
+		if (!pg_constraint_ownercheck(assertionOid, GetUserId()) &&
+			!pg_namespace_ownercheck(con->connamespace, GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CONSTRAINT,
+						   NameStr(con->conname));
+
+		object.classId = ConstraintRelationId;
+		object.objectId = assertionOid;
+		object.objectSubId = 0;
+
+		add_exact_object_address(&object, objects);
+
+		ReleaseSysCache(tuple);
+	}
+
+	performMultipleDeletions(objects, drop->behavior, 0);
+
+	free_object_addresses(objects);
+}
+
+
+Oid
+RenameAssertion(List *name, const char *newname)
+{
+	Oid			assertionOid;
+	Relation	rel;
+	HeapTuple	tuple;
+	Form_pg_constraint con;
+	AclResult	aclresult;
+
+	rel = heap_open(ConstraintRelationId, RowExclusiveLock);
+
+	assertionOid = get_assertion_oid(name, false);
+
+	tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(assertionOid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for constraint %u",
+			 assertionOid);
+	con = (Form_pg_constraint) GETSTRUCT(tuple);
+
+	/* must be owner */
+	if (!pg_constraint_ownercheck(assertionOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CONVERSION,
+					   NameListToString(name));
+
+	/* must have CREATE privilege on namespace */
+	aclresult = pg_namespace_aclcheck(con->connamespace, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
+					   get_namespace_name(con->connamespace));
+
+	ReleaseSysCache(tuple);
+
+	RenameConstraintById(assertionOid, newname);
+
+	heap_close(rel, NoLock);
+
+	return assertionOid;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 328e2a8..8aa48ef 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -68,6 +68,7 @@ typedef enum
 
 static event_trigger_support_data event_trigger_support[] = {
 	{"AGGREGATE", true},
+	{"ASSERTION", true},
 	{"CAST", true},
 	{"CONSTRAINT", true},
 	{"COLLATION", true},
@@ -914,6 +915,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 			/* no support for event triggers on event triggers */
 			return false;
 		case OBJECT_AGGREGATE:
+		case OBJECT_ASSERTION:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
 		case OBJECT_COLUMN:
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 1733da6..7076bf4 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3668,6 +3668,17 @@ _copyCreateSchemaStmt(const CreateSchemaStmt *from)
 	return newnode;
 }
 
+static CreateAssertionStmt *
+_copyCreateAssertionStmt(const CreateAssertionStmt *from)
+{
+	CreateAssertionStmt *newnode = makeNode(CreateAssertionStmt);
+
+	COPY_NODE_FIELD(assertion_name);
+	COPY_NODE_FIELD(constraint);
+
+	return newnode;
+}
+
 static CreateConversionStmt *
 _copyCreateConversionStmt(const CreateConversionStmt *from)
 {
@@ -4453,6 +4464,9 @@ copyObject(const void *from)
 		case T_CreateSchemaStmt:
 			retval = _copyCreateSchemaStmt(from);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _copyCreateAssertionStmt(from);
+			break;
 		case T_CreateConversionStmt:
 			retval = _copyCreateConversionStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 7b29812..7dd5ce4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1874,6 +1874,15 @@ _equalCreateSchemaStmt(const CreateSchemaStmt *a, const CreateSchemaStmt *b)
 }
 
 static bool
+_equalCreateAssertionStmt(const CreateAssertionStmt *a, const CreateAssertionStmt *b)
+{
+	COMPARE_NODE_FIELD(assertion_name);
+	COMPARE_NODE_FIELD(constraint);
+
+	return true;
+}
+
+static bool
 _equalCreateConversionStmt(const CreateConversionStmt *a, const CreateConversionStmt *b)
 {
 	COMPARE_NODE_FIELD(conversion_name);
@@ -2921,6 +2930,9 @@ equal(const void *a, const void *b)
 		case T_CreateSchemaStmt:
 			retval = _equalCreateSchemaStmt(a, b);
 			break;
+		case T_CreateAssertionStmt:
+			retval = _equalCreateAssertionStmt(a, b);
+			break;
 		case T_CreateConversionStmt:
 			retval = _equalCreateConversionStmt(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 11f6291..5ce530c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -227,11 +227,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-		CreateAssertStmt CreateTrigStmt CreateEventTrigStmt
+		CreateAssertionStmt CreateTrigStmt CreateEventTrigStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
-		DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt
+		DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt
 		DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
 		DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt
 		GrantStmt GrantRoleStmt IndexStmt InsertStmt ListenStmt LoadStmt
@@ -738,7 +738,7 @@ stmt :
 			| ConstraintsSetStmt
 			| CopyStmt
 			| CreateAsStmt
-			| CreateAssertStmt
+			| CreateAssertionStmt
 			| CreateCastStmt
 			| CreateConversionStmt
 			| CreateDomainStmt
@@ -769,7 +769,6 @@ stmt :
 			| DeleteStmt
 			| DiscardStmt
 			| DoStmt
-			| DropAssertStmt
 			| DropCastStmt
 			| DropFdwStmt
 			| DropForeignServerStmt
@@ -4601,45 +4600,29 @@ enable_trigger:
  *
  *		QUERIES :
  *				CREATE ASSERTION ...
- *				DROP ASSERTION ...
  *
  *****************************************************************************/
 
-CreateAssertStmt:
-			CREATE ASSERTION name CHECK '(' a_expr ')'
-			ConstraintAttributeSpec
+CreateAssertionStmt:
+			CREATE ASSERTION any_name CHECK '(' a_expr ')' ConstraintAttributeSpec
 				{
-					CreateTrigStmt *n = makeNode(CreateTrigStmt);
-					n->trigname = $3;
-					n->args = list_make1($6);
-					n->isconstraint  = TRUE;
+					CreateAssertionStmt *n = makeNode(CreateAssertionStmt);
+					Constraint *c = makeNode(Constraint);
+
+					c->contype = CONSTR_CHECK;
+					c->location = @4;
+					c->raw_expr = $6;
+					c->cooked_expr = NULL;
 					processCASbits($8, @8, "ASSERTION",
-								   &n->deferrable, &n->initdeferred, NULL,
+								   &c->deferrable, &c->initdeferred, NULL,
 								   NULL, yyscanner);
 
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("CREATE ASSERTION is not yet implemented")));
-
+					n->assertion_name = $3;
+					n->constraint = c;
 					$$ = (Node *)n;
 				}
 		;
 
-DropAssertStmt:
-			DROP ASSERTION name opt_drop_behavior
-				{
-					DropStmt *n = makeNode(DropStmt);
-					n->objects = NIL;
-					n->arguments = NIL;
-					n->behavior = $4;
-					n->removeType = OBJECT_TRIGGER; /* XXX */
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("DROP ASSERTION is not yet implemented")));
-					$$ = (Node *) n;
-				}
-		;
-
 
 /*****************************************************************************
  *
@@ -5190,6 +5173,7 @@ drop_type:	TABLE									{ $$ = OBJECT_TABLE; }
 			| EVENT TRIGGER 						{ $$ = OBJECT_EVENT_TRIGGER; }
 			| TYPE_P								{ $$ = OBJECT_TYPE; }
 			| DOMAIN_P								{ $$ = OBJECT_DOMAIN; }
+			| ASSERTION								{ $$ = OBJECT_ASSERTION; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| SCHEMA								{ $$ = OBJECT_SCHEMA; }
@@ -5400,6 +5384,7 @@ comment_type:
 			| TYPE_P							{ $$ = OBJECT_TYPE; }
 			| VIEW								{ $$ = OBJECT_VIEW; }
 			| MATERIALIZED VIEW					{ $$ = OBJECT_MATVIEW; }
+			| ASSERTION							{ $$ = OBJECT_ASSERTION; }
 			| COLLATION							{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P						{ $$ = OBJECT_CONVERSION; }
 			| TABLESPACE						{ $$ = OBJECT_TABLESPACE; }
@@ -6889,6 +6874,14 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+					n->renameType = OBJECT_ASSERTION;
+					n->object = $3;
+					n->newname = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -7363,6 +7356,14 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = $3;
+					n->newschema = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -7591,6 +7592,14 @@ AlterOwnerStmt: ALTER AGGREGATE func_name aggr_args OWNER TO RoleId
 					n->newowner = $7;
 					$$ = (Node *)n;
 				}
+			| ALTER ASSERTION any_name OWNER TO RoleId
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+					n->objectType = OBJECT_ASSERTION;
+					n->object = $3;
+					n->newowner = $6;
+					$$ = (Node *)n;
+				}
 			| ALTER COLLATION any_name OWNER TO RoleId
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 98cb58a..0e6bd60 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -251,6 +251,7 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			err = _("aggregate functions are not allowed in check constraints");
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
@@ -526,6 +527,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			err = _("window functions are not allowed in check constraints");
 			break;
 		case EXPR_KIND_COLUMN_DEFAULT:
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 68b711d..ec8046f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1459,6 +1459,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ASSERTION_CHECK:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2626,6 +2627,7 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "VALUES";
 		case EXPR_KIND_CHECK_CONSTRAINT:
 		case EXPR_KIND_DOMAIN_CHECK:
+		case EXPR_KIND_ASSERTION_CHECK:
 			return "CHECK";
 		case EXPR_KIND_COLUMN_DEFAULT:
 		case EXPR_KIND_FUNCTION_DEFAULT:
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6a7bf0d..d9a739c 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -28,6 +28,7 @@
 #include "commands/cluster.h"
 #include "commands/comment.h"
 #include "commands/collationcmds.h"
+#include "commands/constraint.h"
 #include "commands/conversioncmds.h"
 #include "commands/copy.h"
 #include "commands/createas.h"
@@ -194,6 +195,7 @@ check_xact_readonly(Node *parsetree)
 		case T_RenameStmt:
 		case T_CommentStmt:
 		case T_DefineStmt:
+		case T_CreateAssertionStmt:
 		case T_CreateCastStmt:
 		case T_CreateEventTrigStmt:
 		case T_AlterEventTrigStmt:
@@ -1276,6 +1278,10 @@ ProcessUtilitySlow(Node *parsetree,
 				DefineDomain((CreateDomainStmt *) parsetree);
 				break;
 
+			case T_CreateAssertionStmt:
+				CreateAssertion((CreateAssertionStmt *) parsetree);
+				break;
+
 			case T_CreateConversionStmt:
 				CreateConversionCommand((CreateConversionStmt *) parsetree);
 				break;
@@ -1574,6 +1580,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_AGGREGATE:
 			tag = "ALTER AGGREGATE";
 			break;
+		case OBJECT_ASSERTION:
+			tag = "ALTER ASSERTION";
+			break;
 		case OBJECT_ATTRIBUTE:
 			tag = "ALTER TYPE";
 			break;
@@ -1885,6 +1894,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_DOMAIN:
 					tag = "DROP DOMAIN";
 					break;
+				case OBJECT_ASSERTION:
+					tag = "DROP ASSERTION";
+					break;
 				case OBJECT_COLLATION:
 					tag = "DROP COLLATION";
 					break;
@@ -2255,6 +2267,10 @@ CreateCommandTag(Node *parsetree)
 			tag = "REINDEX";
 			break;
 
+		case T_CreateAssertionStmt:
+			tag = "CREATE ASSERTION";
+			break;
+
 		case T_CreateConversionStmt:
 			tag = "CREATE CONVERSION";
 			break;
@@ -2798,6 +2814,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_ALL;	/* should this be DDL? */
 			break;
 
+		case T_CreateAssertionStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreateConversionStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index d0e25a9..c6b9aac 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -358,6 +358,9 @@ exec_command(const char *cmd,
 			case 'a':
 				success = describeAggregates(pattern, show_verbose, show_system);
 				break;
+			case 'A':
+				success = describeAssertions(pattern);
+				break;
 			case 'b':
 				success = describeTablespaces(pattern, show_verbose);
 				break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ceda13e..f128388 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -129,6 +129,65 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/* \dA
+ * Takes an optional regexp to select particular assertions
+ */
+bool
+describeAssertions(const char *pattern)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 90400)
+	{
+		fprintf(stderr, _("The server (version %d.%d) does not support assertions.\n"),
+				pset.sversion / 10000, (pset.sversion / 100) % 100);
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT n.nspname AS \"%s\",\n"
+					  "  c.conname AS \"%s\",\n"
+					  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS \"%s\",\n"
+					  "  pg_catalog.obj_description(c.oid, 'pg_constraint') AS \"%s\"\n"
+					  "FROM pg_catalog.pg_constraint c\n"
+					  "     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\n"
+					  "WHERE c.conrelid = 0 AND c.contypid = 0\n",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Definition"),
+					  gettext_noop("Description"));
+
+	if (!pattern)
+		appendPQExpBuffer(&buf, "      AND n.nspname <> 'pg_catalog'\n"
+						  "      AND n.nspname <> 'information_schema'\n");
+
+#if TODO
+	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+						  "n.nspname", "c.conname", NULL,
+						  "pg_catalog.pg_constraint_is_visible(c.oid)");
+#endif
+
+	appendPQExpBuffer(&buf, "ORDER BY 1, 2, 3, 4;");
+
+	res = PSQLexec(buf.data, false);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of assertions");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /* \db
  * Takes an optional regexp to select particular tablespaces
  */
@@ -1926,6 +1985,38 @@ describeOneTableDetails(const char *schemaname,
 			PQclear(result);
 		}
 
+		/* print assertions referencing this table (none if no triggers) */
+		if (tableinfo.hastriggers)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT conname,\n"
+							  "  pg_catalog.pg_get_constraintdef(c.oid, true) AS condef\n"
+							  "FROM pg_catalog.pg_constraint c\n"
+							  "WHERE c.oid IN (SELECT objid FROM pg_depend WHERE classid = 'pg_constraint'::pg_catalog.regclass AND refclassid = 'pg_class'::pg_catalog.regclass AND refobjid = '%s')\n"
+							  "  AND c.conrelid = 0 AND c.contypid = 0 AND c.contype = 'c'\n"
+							  "ORDER BY 1",
+							  oid);
+			result = PSQLexec(buf.data, false);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Assertions:"));
+				for (i = 0; i < tuples; i++)
+				{
+					printfPQExpBuffer(&buf, "    \"%s\" %s",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print rules */
 		if (tableinfo.hasrules && tableinfo.relkind != 'm')
 		{
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 09b6237..7b83d61 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -12,6 +12,9 @@
 /* \da */
 extern bool describeAggregates(const char *pattern, bool verbose, bool showSystem);
 
+/* \dA */
+extern bool describeAssertions(const char *pattern);
+
 /* \db */
 extern bool describeTablespaces(const char *pattern, bool verbose);
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 30530f2..410a7f5 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -203,6 +203,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\d[S+]                 list tables, views, and sequences\n"));
 	fprintf(output, _("  \\d[S+]  NAME           describe table, view, sequence, or index\n"));
 	fprintf(output, _("  \\da[S]  [PATTERN]      list aggregates\n"));
+	fprintf(output, _("  \\dA     [PATTERN]      list assertions\n"));
 	fprintf(output, _("  \\db[+]  [PATTERN]      list tablespaces\n"));
 	fprintf(output, _("  \\dc[S+] [PATTERN]      list conversions\n"));
 	fprintf(output, _("  \\dC[+]  [PATTERN]      list casts\n"));
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f6c1aaf..efdadd5 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -247,6 +247,21 @@ static const SchemaQuery Query_for_list_of_aggregates = {
 	NULL
 };
 
+static const SchemaQuery Query_for_list_of_assertions = {
+	/* catname */
+	"pg_catalog.pg_constraint c",
+	/* selcondition */
+	"(c.conrelid = 0 AND c.contypid = 0)",
+	/* viscondition */
+	"TRUE", //TODO: "pg_catalog.pg_constraint_is_visible(c.oid)",
+	/* namespace */
+	"c.connamespace",
+	/* result */
+	"pg_catalog.quote_ident(c.conname)",
+	/* qualresult */
+	NULL
+};
+
 static const SchemaQuery Query_for_list_of_datatypes = {
 	/* catname */
 	"pg_catalog.pg_type t",
@@ -727,6 +742,7 @@ typedef struct
 
 static const pgsql_thing_t words_after_create[] = {
 	{"AGGREGATE", NULL, &Query_for_list_of_aggregates},
+	{"ASSERTION", NULL, &Query_for_list_of_assertions},
 	{"CAST", NULL, NULL},		/* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", "SELECT pg_catalog.quote_ident(collname) FROM pg_catalog.pg_collation WHERE collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding())) AND substring(pg_catalog.quote_ident(collname),1,%d)='%s'"},
@@ -857,7 +873,7 @@ psql_completion(char *text, int start, int end)
 
 	static const char *const backslash_commands[] = {
 		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
-		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\df",
+		"\\d", "\\da", "\\dA", "\\db", "\\dc", "\\dC", "\\dd", "\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dn", "\\do", "\\dp", "\\drds", "\\ds", "\\dS", "\\dt", "\\dT", "\\dv", "\\du", "\\dx",
 		"\\e", "\\echo", "\\ef", "\\encoding",
@@ -927,7 +943,7 @@ psql_completion(char *text, int start, int end)
 			 pg_strcasecmp(prev3_wd, "TABLE") != 0)
 	{
 		static const char *const list_ALTER[] =
-		{"AGGREGATE", "COLLATION", "CONVERSION", "DATABASE", "DEFAULT PRIVILEGES", "DOMAIN",
+		{"AGGREGATE", "ASSERTION", "COLLATION", "CONVERSION", "DATABASE", "DEFAULT PRIVILEGES", "DOMAIN",
 			"EXTENSION", "FOREIGN DATA WRAPPER", "FOREIGN TABLE", "FUNCTION",
 			"GROUP", "INDEX", "LANGUAGE", "LARGE OBJECT", "MATERIALIZED VIEW", "OPERATOR",
 			"ROLE", "RULE", "SCHEMA", "SERVER", "SEQUENCE", "TABLE",
@@ -967,6 +983,16 @@ psql_completion(char *text, int start, int end)
 		COMPLETE_WITH_LIST(list_ALTERGEN);
 	}
 
+	/* ALTER ASSERTION <name> */
+	else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 &&
+			 pg_strcasecmp(prev2_wd, "ASSERTION") == 0)
+	{
+		static const char *const list_ALTERGEN[] =
+		{"OWNER TO", "RENAME TO", "SET SCHEMA", NULL};
+
+		COMPLETE_WITH_LIST(list_ALTERGEN);
+	}
+
 	/* ALTER COLLATION <name> */
 	else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 &&
 			 pg_strcasecmp(prev2_wd, "COLLATION") == 0)
@@ -2398,7 +2424,8 @@ psql_completion(char *text, int start, int end)
 
 	/* DROP object with CASCADE / RESTRICT */
 	else if ((pg_strcasecmp(prev3_wd, "DROP") == 0 &&
-			  (pg_strcasecmp(prev2_wd, "COLLATION") == 0 ||
+			  (pg_strcasecmp(prev2_wd, "ASSERTION") == 0 ||
+			   pg_strcasecmp(prev2_wd, "COLLATION") == 0 ||
 			   pg_strcasecmp(prev2_wd, "CONVERSION") == 0 ||
 			   pg_strcasecmp(prev2_wd, "DOMAIN") == 0 ||
 			   pg_strcasecmp(prev2_wd, "EXTENSION") == 0 ||
@@ -3271,6 +3298,8 @@ psql_completion(char *text, int start, int end)
 
 	else if (strncmp(prev_wd, "\\da", strlen("\\da")) == 0)
 		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_aggregates, NULL);
+	else if (strncmp(prev_wd, "\\dA", strlen("\\dA")) == 0)
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_assertions, NULL);
 	else if (strncmp(prev_wd, "\\db", strlen("\\db")) == 0)
 		COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
 	else if (strncmp(prev_wd, "\\dD", strlen("\\dD")) == 0)
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 4fadc74..0a1e325 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -132,6 +132,7 @@ extern bool OverrideSearchPathMatchesCurrent(OverrideSearchPath *path);
 extern void PushOverrideSearchPath(OverrideSearchPath *newpath);
 extern void PopOverrideSearchPath(void);
 
+extern Oid	get_assertion_oid(List *name, bool missing_ok);
 extern Oid	get_collation_oid(List *collname, bool missing_ok);
 extern Oid	get_conversion_oid(List *conname, bool missing_ok);
 extern Oid	FindDefaultConversionProc(int32 for_encoding, int32 to_encoding);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index e1dba46..f27d7c4 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -59,8 +59,7 @@ CATALOG(pg_constraint,2606)
 	 * contypid links to the pg_type row for a domain if this is a domain
 	 * constraint.	Otherwise it's 0.
 	 *
-	 * For SQL-style global ASSERTIONs, both conrelid and contypid would be
-	 * zero. This is not presently supported, however.
+	 * For SQL-style global ASSERTIONs, both conrelid and contypid are zero.
 	 */
 	Oid			contypid;		/* domain this constraint constrains */
 
@@ -200,7 +199,7 @@ typedef enum ConstraintCategory
 {
 	CONSTRAINT_RELATION,
 	CONSTRAINT_DOMAIN,
-	CONSTRAINT_ASSERTION		/* for future expansion */
+	CONSTRAINT_ASSERTION
 } ConstraintCategory;
 
 /*
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index ca4fc62..35b37ca 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -1994,6 +1994,9 @@ DESCR("is a column updatable");
 DATA(insert OID = 1250 (  unique_key_recheck	PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ ));
 DESCR("deferred UNIQUE constraint check");
 
+DATA(insert OID = 3789 (  assertion_check	PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ assertion_check _null_ _null_ _null_ ));
+DESCR("assertion check");
+
 /* Generic referential integrity constraint triggers */
 DATA(insert OID = 1644 (  RI_FKey_check_ins		PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ RI_FKey_check_ins _null_ _null_ _null_ ));
 DESCR("referential integrity FOREIGN KEY ... REFERENCES");
diff --git a/src/include/commands/constraint.h b/src/include/commands/constraint.h
new file mode 100644
index 0000000..e76f483
--- /dev/null
+++ b/src/include/commands/constraint.h
@@ -0,0 +1,10 @@
+#ifndef CONSTRAINT_H
+#define CONSTRAINT_H
+
+#include "nodes/parsenodes.h"
+
+extern void CreateAssertion(CreateAssertionStmt *stmt);
+extern void DropAssertion(DropStmt *drop);
+extern Oid RenameAssertion(List *name, const char *newname);
+
+#endif
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index fc6b1d7..6cbcaa5 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -363,6 +363,7 @@ typedef enum NodeTag
 	T_AlterEventTrigStmt,
 	T_RefreshMatViewStmt,
 	T_ReplicaIdentityStmt,
+	T_CreateAssertionStmt,
 
 	/*
 	 * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 55524b4..67d2232 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1157,6 +1157,7 @@ typedef struct SetOperationStmt
 typedef enum ObjectType
 {
 	OBJECT_AGGREGATE,
+	OBJECT_ASSERTION,
 	OBJECT_ATTRIBUTE,			/* type's attribute, when distinct from column */
 	OBJECT_CAST,
 	OBJECT_COLUMN,
@@ -2599,6 +2600,17 @@ typedef struct ReindexStmt
 } ReindexStmt;
 
 /* ----------------------
+ *		CREATE ASSERTION Statement
+ * ----------------------
+ */
+typedef struct CreateAssertionStmt
+{
+	NodeTag		type;
+	List	   *assertion_name;
+	Constraint *constraint;
+} CreateAssertionStmt;
+
+/* ----------------------
  *		CREATE CONVERSION Statement
  * ----------------------
  */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index bea3b07..4c8484b 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -57,6 +57,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_VALUES,			/* VALUES */
 	EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */
 	EXPR_KIND_DOMAIN_CHECK,		/* CHECK constraint for a domain */
+	EXPR_KIND_ASSERTION_CHECK,	/* CHECK constraint in assertion */
 	EXPR_KIND_COLUMN_DEFAULT,	/* default value for a table column */
 	EXPR_KIND_FUNCTION_DEFAULT, /* default parameter value for function */
 	EXPR_KIND_INDEX_EXPRESSION, /* index expression */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 2116259..3431029 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -189,6 +189,7 @@ typedef enum AclObjectKind
 	ACL_KIND_OPCLASS,			/* pg_opclass */
 	ACL_KIND_OPFAMILY,			/* pg_opfamily */
 	ACL_KIND_COLLATION,			/* pg_collation */
+	ACL_KIND_CONSTRAINT,		/* pg_constraint */
 	ACL_KIND_CONVERSION,		/* pg_conversion */
 	ACL_KIND_TABLESPACE,		/* pg_tablespace */
 	ACL_KIND_TSDICTIONARY,		/* pg_ts_dict */
@@ -318,6 +319,7 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_constraint_ownercheck(Oid constr_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index ce3f00b..98c520f 100644
--- a/src/include/utils/builtins.h
+++ b/src/include/utils/builtins.h
@@ -1157,6 +1157,7 @@ extern Datum pg_identify_object(PG_FUNCTION_ARGS);
 
 /* commands/constraint.c */
 extern Datum unique_key_recheck(PG_FUNCTION_ARGS);
+extern Datum assertion_check(PG_FUNCTION_ARGS);
 
 /* commands/event_trigger.c */
 extern Datum pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS);
diff --git a/src/test/regress/expected/assertions.out b/src/test/regress/expected/assertions.out
new file mode 100644
index 0000000..d09c6d0
--- /dev/null
+++ b/src/test/regress/expected/assertions.out
@@ -0,0 +1,58 @@
+CREATE TABLE test1 (a int, b text);
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION wrong CHECK (1 + 2);
+ERROR:  argument of CHECK must be type boolean, not type integer
+CREATE ASSERTION wrong CHECK (a < 5);
+ERROR:  column "a" does not exist
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+CREATE ASSERTION wrong CHECK ((SELECT count(*) FROM wrong) < 5);
+ERROR:  relation "wrong" does not exist
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+INSERT INTO test1 VALUES (5, 'five');
+ERROR:  assertion "a2" violated
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+ public            | a2
+ public            | foo
+(2 rows)
+
+\dA
+                        List of assertions
+ Schema | Name |             Definition             | Description 
+--------+------+------------------------------------+-------------
+ public | a2   | CHECK ((( SELECT count(*) AS count+| 
+        |      |    FROM test1)) < 5)               | 
+ public | foo  | CHECK (1 < 2)                      | 
+(2 rows)
+
+ALTER ASSERTION a2 RENAME TO a3;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ERROR:  assertion "a3" already exists
+ALTER ASSERTION wrong RENAME TO wrong2; -- fails
+ERROR:  assertion "wrong" does not exist
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+ public            | a3
+ public            | foo
+(2 rows)
+
+DROP ASSERTION foo;
+DROP ASSERTION wrong;
+ERROR:  assertion "wrong" does not exist
+DROP TABLE test1; -- fails
+ERROR:  cannot drop table test1 because other objects depend on it
+DETAIL:  constraint a3 depends on table test1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+DROP TABLE test1 CASCADE;
+NOTICE:  drop cascades to constraint a3
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
+ constraint_schema | constraint_name 
+-------------------+-----------------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5758b07..baeb57f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -59,7 +59,7 @@ test: create_index create_view
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views
+test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views assertions
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 78348f5..87c8708 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -52,6 +52,7 @@ test: create_function_1
 test: create_type
 test: create_table
 test: create_function_2
+test: assertions
 test: copy
 test: copyselect
 test: create_misc
diff --git a/src/test/regress/sql/assertions.sql b/src/test/regress/sql/assertions.sql
new file mode 100644
index 0000000..247f74d
--- /dev/null
+++ b/src/test/regress/sql/assertions.sql
@@ -0,0 +1,31 @@
+CREATE TABLE test1 (a int, b text);
+
+CREATE ASSERTION foo CHECK (1 < 2);
+CREATE ASSERTION wrong CHECK (1 + 2);
+CREATE ASSERTION wrong CHECK (a < 5);
+CREATE ASSERTION a2 CHECK ((SELECT count(*) FROM test1) < 5);
+CREATE ASSERTION wrong CHECK ((SELECT count(*) FROM wrong) < 5);
+
+DELETE FROM test1;
+INSERT INTO test1 VALUES (1, 'one');
+INSERT INTO test1 VALUES (2, 'two');
+INSERT INTO test1 VALUES (3, 'three');
+INSERT INTO test1 VALUES (4, 'four');
+INSERT INTO test1 VALUES (5, 'five');
+
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
+\dA
+
+ALTER ASSERTION a2 RENAME TO a3;
+ALTER ASSERTION foo RENAME TO a3; -- fails
+ALTER ASSERTION wrong RENAME TO wrong2; -- fails
+
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
+
+DROP ASSERTION foo;
+DROP ASSERTION wrong;
+
+DROP TABLE test1; -- fails
+DROP TABLE test1 CASCADE;
+
+SELECT constraint_schema, constraint_name FROM information_schema.assertions ORDER BY 1, 2;
-- 
1.8.4.2

#2Andreas Karlsson
andreas@proxel.se
In reply to: Steeve Lennmark (#1)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On 01/09/2014 06:58 PM, Steeve Lennmark wrote:

This patch adds the ability to relocate tablespaces by adding the
command line argument --tablespace (-T) which takes a required argument
in the format "oid:tablespacedir". After all tablespaces are fetched
this code updates the symlink to point to the new tablespace location.

I would have loved to be able to pass tablespacename:tablespacedir
though, but sadly I wasn't able to figure out how to retrieve that
information without creating another connection to the database.

This feature would be a nice addition to pg_basebackup, and I agree with
that it would be preferable to use names of oids if possible.

This feature might be missing because of some other limitation I fail
to see, if so let me know. Please be gentle, this is my first patch ;-)

It seems like you have attached the wrong patch. The only attachment I
see is 0001-SQL-assertions-prototype.patch.

Best regards,
Andreas

--
Andreas Karlsson

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Andreas Karlsson (#2)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Yes, apparently my virgin flight crashed and burn. I here attach the
correct file!

//Steeve

On Thu, Jan 9, 2014 at 7:16 PM, Andreas Karlsson <andreas@proxel.se> wrote:

Show quoted text

On 01/09/2014 06:58 PM, Steeve Lennmark wrote:

This patch adds the ability to relocate tablespaces by adding the

command line argument --tablespace (-T) which takes a required argument
in the format "oid:tablespacedir". After all tablespaces are fetched
this code updates the symlink to point to the new tablespace location.

I would have loved to be able to pass tablespacename:tablespacedir
though, but sadly I wasn't able to figure out how to retrieve that
information without creating another connection to the database.

This feature would be a nice addition to pg_basebackup, and I agree with
that it would be preferable to use names of oids if possible.

This feature might be missing because of some other limitation I fail

to see, if so let me know. Please be gentle, this is my first patch ;-)

It seems like you have attached the wrong patch. The only attachment I see
is 0001-SQL-assertions-prototype.patch.

Best regards,
Andreas

--
Andreas Karlsson

Attachments:

0001-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0001-pg_basebackup-relocate-tablespace.patchDownload
*** doc/src/sgml/ref/pg_basebackup.sgml
--- doc/src/sgml/ref/pg_basebackup.sgml
***************
*** 138,143 **** PostgreSQL documentation
--- 138,155 ----
       </varlistentry>
  
       <varlistentry>
+       <term><option>-T <replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+       <term><option>--tablespace=<replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+       <listitem>
+        <para>
+         Specifies the location where tablespace with <replaceable>oid</replaceable>
+         is written, <replaceable>tablespacedir</replaceable> must be an absolute path.
+         This options can be specified multiple times for multiple tablespaces.
+        </para>
+       </listitem>
+      </varlistentry>
+ 
+      <varlistentry>
        <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
        <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
        <listitem>
***************
*** 530,536 **** PostgreSQL documentation
    <para>
     The way <productname>PostgreSQL</productname> manages tablespaces, the path
     for all additional tablespaces must be identical whenever a backup is
!    restored. The main data directory, however, is relocatable to any location.
    </para>
  
    <para>
--- 542,548 ----
    <para>
     The way <productname>PostgreSQL</productname> manages tablespaces, the path
     for all additional tablespaces must be identical whenever a backup is
!    restored, if <replaceable>--tablespace</replaceable> isn't specified.
    </para>
  
    <para>
***************
*** 570,575 **** PostgreSQL documentation
--- 582,595 ----
     (This command will fail if there are multiple tablespaces in the
     database.)
    </para>
+ 
+   <para>
+    To create a backup of a two-tablespace local database where tablespace
+    <literal>archive</literal> is written to <literal>./backup/archive</literal>
+ <screen>
+ <prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T archive:$(pwd)/backup/archive</userinput>
+ </screen>
+   </para>
   </refsect1>
  
   <refsect1>
*** src/bin/pg_basebackup/pg_basebackup.c
--- src/bin/pg_basebackup/pg_basebackup.c
***************
*** 33,40 ****
--- 33,54 ----
  #include "streamutil.h"
  
  
+ #define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+ 
+ typedef struct TablespaceListCell {
+ 	struct TablespaceListCell *next;
+ 	Oid oid;
+ 	char path[1];
+ } TablespaceListCell;
+ 
+ typedef struct TablespaceList {
+ 	TablespaceListCell *head;
+ 	TablespaceListCell *tail;
+ } TablespaceList;
+ 
  /* Global options */
  static char *basedir = NULL;
+ static TablespaceList tablespace_locations = {NULL, NULL};
  static char *xlog_dir = "";
  static char	format = 'p';		/* p(lain)/t(ar) */
  static char *label = "pg_basebackup base backup";
***************
*** 86,91 **** static void BaseBackup(void);
--- 100,140 ----
  static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
  					 bool segment_finished);
  
+ static const char *get_tablespace_location(Oid oid, const char *location);
+ static void update_tablespace_symlink(Oid oid, const char *location);
+ static bool tablespace_list_append(TablespaceList *list, const char *tablespace);
+ 
+ static bool
+ tablespace_list_append(TablespaceList *list, const char *tablespace)
+ {
+ 	TablespaceListCell *cell;
+ 	char *path;
+ 	Oid oid = atooid(tablespace);
+ 
+ 	if (!oid)
+ 		return false;
+ 
+ 	path = strstr(tablespace, ":/");
+ 	if (!path)
+ 		return false;
+ 	path++;
+ 
+ 	cell = (TablespaceListCell *) pg_malloc(sizeof(TablespaceListCell) + strlen(path));
+ 
+ 	cell->next = NULL;
+ 	cell->oid = oid;
+ 	strcpy(cell->path, path);
+ 
+ 	if (list->tail)
+ 		list->tail->next = cell;
+ 	else
+ 		list->head = cell;
+ 	list->tail = cell;
+ 
+ 	return true;
+ }
+ 
+ 
  #ifdef HAVE_LIBZ
  static const char *
  get_gz_error(gzFile gzf)
***************
*** 110,115 **** usage(void)
--- 159,166 ----
  	printf(_("  %s [OPTION]...\n"), progname);
  	printf(_("\nOptions controlling the output:\n"));
  	printf(_("  -D, --pgdata=DIRECTORY receive base backup into directory\n"));
+ 	printf(_("  -T, --tablespace=OID:LOCATION\n"
+ 			 "                         Specify tablespace oid and its absolute location\n"));
  	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
  	printf(_("  -R, --write-recovery-conf\n"
  			 "                         write recovery.conf after backup\n"));
***************
*** 861,874 **** ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
  }
  
  /*
   * Receive a tar format stream from the connection to the server, and unpack
   * the contents of it into a directory. Only files, directories and
   * symlinks are supported, no other kinds of special files.
   *
   * If the data is for the main data directory, it will be restored in the
   * specified directory. If it's for another tablespace, it will be restored
!  * in the original directory, since relocation of tablespaces is not
!  * supported.
   */
  static void
  ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
--- 912,967 ----
  }
  
  /*
+  * Retrieve tablespace location, either relocated or original depending on
+  * whether -T oid:tablespacedir was passed or not.
+  */
+ static const char *
+ get_tablespace_location(Oid oid, const char *location)
+ {
+ 	TablespaceListCell *cell;
+ 
+ 	for (cell = tablespace_locations.head; cell; cell = cell->next)
+ 		if (oid == cell->oid)
+ 			return cell->path;
+ 
+ 	return location;
+ }
+ 
+ /*
+  * Update symlinks to reflect relocated tablespace, only applied if
+  * tablespace isn't in its original location.
+  */
+ static void
+ update_tablespace_symlink(Oid oid, const char *location)
+ {
+ 	const char *new_location = get_tablespace_location(oid, location);
+ 	if (strcmp(new_location, location) != 0)
+ 	{
+ 		char linkloc[MAXPGPATH];
+ 		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+ 		if (unlink(linkloc) < 0 && errno != ENOENT)
+ 		{
+ 			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+ 					progname, linkloc, strerror(errno));
+ 			disconnect_and_exit(1);
+ 		}
+ 		if (symlink(new_location, linkloc) < 0)
+ 		{
+ 			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+ 					progname, linkloc, strerror(errno));
+ 			disconnect_and_exit(1);
+ 		}
+ 	}
+ }
+ 
+ /*
   * Receive a tar format stream from the connection to the server, and unpack
   * the contents of it into a directory. Only files, directories and
   * symlinks are supported, no other kinds of special files.
   *
   * If the data is for the main data directory, it will be restored in the
   * specified directory. If it's for another tablespace, it will be restored
!  * in the original directory, if tablespace relocation is not enabled.
   */
  static void
  ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
***************
*** 884,890 **** ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
  	if (basetablespace)
  		strcpy(current_path, basedir);
  	else
! 		strcpy(current_path, PQgetvalue(res, rownum, 1));
  
  	/*
  	 * Get the COPY data
--- 977,987 ----
  	if (basetablespace)
  		strcpy(current_path, basedir);
  	else
! 	{
! 		strcpy(current_path,
! 			   get_tablespace_location(atooid(PQgetvalue(res, rownum, 0)),
! 									   PQgetvalue(res, rownum, 1)));
! 	}
  
  	/*
  	 * Get the COPY data
***************
*** 1465,1471 **** BaseBackup(void)
  		 * we do anything anyway.
  		 */
  		if (format == 'p' && !PQgetisnull(res, i, 1))
! 			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
  	}
  
  	/*
--- 1562,1572 ----
  		 * we do anything anyway.
  		 */
  		if (format == 'p' && !PQgetisnull(res, i, 1))
! 		{
! 			char *path = (char *) get_tablespace_location(atooid(PQgetvalue(res, i, 0)),
! 														  PQgetvalue(res, i, 1));
! 			verify_dir_is_empty_or_create(path);
! 		}
  	}
  
  	/*
***************
*** 1507,1512 **** BaseBackup(void)
--- 1608,1629 ----
  		progress_report(PQntuples(res), NULL);
  		fprintf(stderr, "\n");	/* Need to move to next line */
  	}
+ 
+ 	if (format == 'p' && tablespace_locations.head != NULL)
+ 	{
+ #ifdef HAVE_SYMLINK
+ 		for (i = 0; i < PQntuples(res); i++)
+ 		{
+ 			int tblspc_oid = atooid(PQgetvalue(res, i, 0));
+ 			if (tblspc_oid)
+ 				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+ 		}
+ #else
+ 		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+ 				progname);
+ #endif
+ 	}
+ 
  	PQclear(res);
  
  	/*
***************
*** 1655,1660 **** main(int argc, char **argv)
--- 1772,1778 ----
  		{"help", no_argument, NULL, '?'},
  		{"version", no_argument, NULL, 'V'},
  		{"pgdata", required_argument, NULL, 'D'},
+ 		{"tablespace", required_argument, NULL, 'T'},
  		{"format", required_argument, NULL, 'F'},
  		{"checkpoint", required_argument, NULL, 'c'},
  		{"write-recovery-conf", no_argument, NULL, 'R'},
***************
*** 1697,1703 **** main(int argc, char **argv)
  		}
  	}
  
! 	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
  							long_options, &option_index)) != -1)
  	{
  		switch (c)
--- 1815,1821 ----
  		}
  	}
  
! 	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
  							long_options, &option_index)) != -1)
  	{
  		switch (c)
***************
*** 1705,1710 **** main(int argc, char **argv)
--- 1823,1837 ----
  			case 'D':
  				basedir = pg_strdup(optarg);
  				break;
+ 			case 'T':
+ 				if (!tablespace_list_append(&tablespace_locations, optarg))
+ 				{
+ 					fprintf(stderr,
+ 							_("%s: invalid tablespace format \"%s\", must be \"oid:/absolute-path\"\n"),
+ 							progname, optarg);
+ 					exit(1);
+ 				}
+ 				break;
  			case 'F':
  				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
  					format = 'p';
#4Magnus Hagander
magnus@hagander.net
In reply to: Steeve Lennmark (#1)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Thu, Jan 9, 2014 at 6:58 PM, Steeve Lennmark
<steevel@handeldsbanken.se>wrote:

Currently pg_basebackup is pretty invasive when using tablespaces, at
least using the plain format. This since it requires the tablespace to
be written to the same location as on the server beeing backed up. This
both breaks backing up locally using -Fp (since the tablespace would
be written to the same location) and requires the backup user to have
write permissions in locations it shouldn't need to have access to.

Yeah, this has been sitting on my TODO for a long time :) Glad to see
someone is picking it up.

This patch adds the ability to relocate tablespaces by adding the

command line argument --tablespace (-T) which takes a required argument
in the format "oid:tablespacedir". After all tablespaces are fetched
this code updates the symlink to point to the new tablespace location.

I would have loved to be able to pass tablespacename:tablespacedir
though, but sadly I wasn't able to figure out how to retrieve that
information without creating another connection to the database.

You could also use the format "olddir:newdir", because you do know that.
It's not the name of the tablespace. but I think it's still more
usefriendly than using the oid.

This feature might be missing because of some other limitation I fail

to see, if so let me know. Please be gentle, this is my first patch ;-)

Nope, I think it's just been limited on time.

--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/

#5Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Magnus Hagander (#4)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Thu, Jan 9, 2014 at 7:18 PM, Magnus Hagander <magnus@hagander.net> wrote:

On Thu, Jan 9, 2014 at 6:58 PM, Steeve Lennmark <steevel@handeldsbanken.se

wrote:

This patch adds the ability to relocate tablespaces by adding the
command line argument --tablespace (-T) which takes a required argument
in the format "oid:tablespacedir". After all tablespaces are fetched
this code updates the symlink to point to the new tablespace location.

I would have loved to be able to pass tablespacename:tablespacedir
though, but sadly I wasn't able to figure out how to retrieve that
information without creating another connection to the database.

You could also use the format "olddir:newdir", because you do know that.
It's not the name of the tablespace. but I think it's still more
usefriendly than using the oid.

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

This produces the following now:
$ ls backup/; ls -l backup/data/pg_tblspc/
data t1 t2
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16388 -> /home/steevel/backup/t1
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16389 -> /home/steevel/backup/t2

--
Steeve Lennmark

Attachments:

0002-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0002-pg_basebackup-relocate-tablespace.patchDownload
commit 8d649148205cf16cee008b4a118654af86b67a0a
Author: Steeve Lennmark <steevel@handeldsbanken.se>
Date:   Thu Jan 9 20:45:26 2014 +0000

    Add support for relocating tablespaces
    
    This is done by mapping old and new tablespace location using the format
    old_dir:new_dir.

diff --git doc/src/sgml/ref/pg_basebackup.sgml doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..cad57ba 100644
--- doc/src/sgml/ref/pg_basebackup.sgml
+++ doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,18 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <term><option>--tablespace=<replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <listitem>
+       <para>
+        Specifies the location where tablespace with <replaceable>oid</replaceable>
+        is written, <replaceable>tablespacedir</replaceable> must be an absolute path.
+        This options can be specified multiple times for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
       <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
       <listitem>
@@ -530,7 +542,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +582,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>archive</literal> is written to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T archive:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git src/bin/pg_basebackup/pg_basebackup.c src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..498ba78 100644
--- src/bin/pg_basebackup/pg_basebackup.c
+++ src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char  *old_dir;
+	char  *new_dir;
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +102,37 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *new_dir);
+static bool tablespace_list_append(TablespaceList *list, char *tablespace);
+
+static bool
+tablespace_list_append(TablespaceList *list, char *tablespace)
+{
+	TablespaceListCell *cell;
+	char *token;
+
+	if (!(token = strtok(tablespace, ":")))
+		return false;
+
+	cell = (TablespaceListCell *) pg_malloc(sizeof(TablespaceListCell));
+	cell->next = NULL;
+	cell->old_dir = pg_strdup(token);
+
+	if (!(token = strtok(NULL, ":")) || token[0] != '/')
+		return false;
+	cell->new_dir = pg_strdup(token);
+
+	if (list->tail)
+		list->tail->next = cell;
+	else
+		list->head = cell;
+	list->tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -110,6 +157,8 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions controlling the output:\n"));
 	printf(_("  -D, --pgdata=DIRECTORY receive base backup into directory\n"));
+	printf(_("  -T, --tablespace=OLDDIR:NEWDIR\n"
+			 "                         relocate tablespace olddir to newdir\n"));
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
@@ -861,14 +910,56 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir:new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+
+	return dir;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char linkloc[MAXPGPATH];
+		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +975,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1556,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1601,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1655,6 +1765,7 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
+		{"tablespace", required_argument, NULL, 'T'},
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
@@ -1697,7 +1808,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1705,6 +1816,15 @@ main(int argc, char **argv)
 			case 'D':
 				basedir = pg_strdup(optarg);
 				break;
+			case 'T':
+				if (!tablespace_list_append(&tablespace_dirs, optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace format \"%s\", must be \"old_dir:/absolute-path\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'F':
 				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
 					format = 'p';
#6Gabriele Bartolini
gabriele.bartolini@2ndQuadrant.it
In reply to: Steeve Lennmark (#5)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Hi Steeve,

Il 09/01/14 22:10, Steeve Lennmark ha scritto:

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

I'd suggest, a similar solution to the one we have adopted in Barman (if
you don't know it: www.pgbarman.org), that is:

--tablespace NAME:LOCATION [--tablespace NAME:location]

I prefer this over the location on the master as this might change over
time (at least more frequently than the tablespace name) and over servers.

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

With the above example, it would become:

$ pg_basebackup -Xs -D backup/data -T tblspc1:$(pwd)/backup/t1 -T
tblspc2:$(pwd)/backup/t2

Thanks,
Gabriele

--
Gabriele Bartolini - 2ndQuadrant Italia
PostgreSQL Training, Services and Support
gabriele.bartolini@2ndQuadrant.it | www.2ndQuadrant.it

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Gabriele Bartolini (#6)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Thu, Jan 9, 2014 at 10:29 PM, Gabriele Bartolini <
gabriele.bartolini@2ndquadrant.it> wrote:

Hi Steeve,

Il 09/01/14 22:10, Steeve Lennmark ha scritto:

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

I'd suggest, a similar solution to the one we have adopted in Barman (if
you don't know it: www.pgbarman.org), that is:

--tablespace NAME:LOCATION [--tablespace NAME:location]

I prefer this over the location on the master as this might change over
time (at least more frequently than the tablespace name) and over servers.

I'm a barman user myself so that was actually my initial thought. If
there aren't some kind of hidden internal that I've missed I don't see
a way to convert an OID (only have OID and path at this stage) to a
tablespace name. This solution, even though not optimal, is a lot
better than my initial one where I used the OID directly.

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

With the above example, it would become:

$ pg_basebackup -Xs -D backup/data -T tblspc1:$(pwd)/backup/t1 -T
tblspc2:$(pwd)/backup/t2

Yeah, that would be my favourite solution.

Regards,
Steeve
--
Steeve Lennmark

#8Gabriele Bartolini
gabriele.bartolini@2ndQuadrant.it
In reply to: Steeve Lennmark (#7)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Hi Steeve,

Il 09/01/14 22:38, Steeve Lennmark ha scritto:

I'm a barman user myself so that was actually my initial thought.

Ah! Very good!

If there aren't some kind of hidden internal that I've missed I don't see
a way to convert an OID (only have OID and path at this stage) to a
tablespace name. This solution, even though not optimal, is a lot
better than my initial one where I used the OID directly.

Try:

SELECT spcname, oid, pg_tablespace_location(oid) FROM pg_tablespace

Thanks,
Gabriele

--
Gabriele Bartolini - 2ndQuadrant Italia
PostgreSQL Training, Services and Support
gabriele.bartolini@2ndQuadrant.it | www.2ndQuadrant.it

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Magnus Hagander
magnus@hagander.net
In reply to: Gabriele Bartolini (#8)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Fri, Jan 10, 2014 at 12:25 PM, Gabriele Bartolini <
gabriele.bartolini@2ndquadrant.it> wrote:

Hi Steeve,

Il 09/01/14 22:38, Steeve Lennmark ha scritto:

I'm a barman user myself so that was actually my initial thought.

Ah! Very good!

If there aren't some kind of hidden internal that I've missed I don't see
a way to convert an OID (only have OID and path at this stage) to a
tablespace name. This solution, even though not optimal, is a lot
better than my initial one where I used the OID directly.

Try:

SELECT spcname, oid, pg_tablespace_location(oid) FROM pg_tablespace

That would require a second connection to the database. You cannot run that
query from the walsender session. And that's exactly the issue that Steeve
pointed out in his first email.

I think it's better to let pg_basebackup work at the lower level, and then
leave it to higher level tools to be able to do the mapping to names.

--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/

#10Andres Freund
andres@2ndquadrant.com
In reply to: Magnus Hagander (#9)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On 2014-01-10 12:27:23 +0100, Magnus Hagander wrote:

On Fri, Jan 10, 2014 at 12:25 PM, Gabriele Bartolini <
gabriele.bartolini@2ndquadrant.it> wrote:

Hi Steeve,

Il 09/01/14 22:38, Steeve Lennmark ha scritto:

I'm a barman user myself so that was actually my initial thought.

Ah! Very good!

If there aren't some kind of hidden internal that I've missed I don't see
a way to convert an OID (only have OID and path at this stage) to a
tablespace name. This solution, even though not optimal, is a lot
better than my initial one where I used the OID directly.

Try:

SELECT spcname, oid, pg_tablespace_location(oid) FROM pg_tablespace

That would require a second connection to the database. You cannot run that
query from the walsender session. And that's exactly the issue that Steeve
pointed out in his first email.

Theoretically nothing is stopping us from providing a command outputting
that information - it's a global catalog, so we can access it without
problems.

I think it's better to let pg_basebackup work at the lower level, and then
leave it to higher level tools to be able to do the mapping to names.

That doesn't negate this argument though. Not really convinced either
way yet.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Andreas Karlsson
andreas@proxel.se
In reply to: Steeve Lennmark (#5)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On 01/09/2014 10:10 PM, Steeve Lennmark wrote:

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

This produces the following now:
$ ls backup/; ls -l backup/data/pg_tblspc/
data t1 t2
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16388 -> /home/steevel/backup/t1
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16389 -> /home/steevel/backup/t2

Looked at the patch quickly and noticed that it does not support paths
containing colons. Is that an acceptable limitation? The $PATH variable
in most UNIX shells does not support paths with colons either so such
naming of directories is already discouraged.

Feel free to add the patch to the upcoming commitfest when you feel it
is ready for a review.

--
Andreas Karlsson

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Andreas Karlsson (#11)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Andreas Karlsson wrote:

On 01/09/2014 10:10 PM, Steeve Lennmark wrote:

That's a much better solution, I attached a patch with the updated code.

Looked at the patch quickly and noticed that it does not support
paths containing colons. Is that an acceptable limitation?

Well, clearly it won't work on Windows when tablespaces are on different
drives, so it doesn't sound so acceptable.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Andreas Karlsson (#11)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Mon, Jan 13, 2014 at 4:29 AM, Andreas Karlsson <andreas@proxel.se> wrote:

On 01/09/2014 10:10 PM, Steeve Lennmark wrote:

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

This produces the following now:
$ ls backup/; ls -l backup/data/pg_tblspc/
data t1 t2
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16388 ->
/home/steevel/backup/t1
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16389 ->
/home/steevel/backup/t2

Looked at the patch quickly and noticed that it does not support paths
containing colons. Is that an acceptable limitation? The $PATH variable in
most UNIX shells does not support paths with colons either so such naming
of directories is already discouraged.

I thought of this too and wrote a patch for that yesterday, I've
attached an updated version which supports passing in a path with
escaped colons.

Feel free to add the patch to the upcoming commitfest when you feel it is

ready for a review.

Done!

Thanks,
--
Steeve Lennmark

Attachments:

0003-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0003-pg_basebackup-relocate-tablespace.patchDownload
commit ee9fab893d9689fffea229ed6be22d13721a1813
Author: Steeve Lennmark <steevel@handeldsbanken.se>
Date:   Thu Jan 9 20:45:26 2014 +0000

    Add support for relocating tablespaces
    
    This is done by mapping old and new tablespace location using the format
    old_dir:new_dir. This also accounts for directory names containing
    colons.

diff --git doc/src/sgml/ref/pg_basebackup.sgml doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..cad57ba 100644
--- doc/src/sgml/ref/pg_basebackup.sgml
+++ doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,18 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <term><option>--tablespace=<replaceable class="parameter">oid:tablespacedir</replaceable></option></term>
+      <listitem>
+       <para>
+        Specifies the location where tablespace with <replaceable>oid</replaceable>
+        is written, <replaceable>tablespacedir</replaceable> must be an absolute path.
+        This options can be specified multiple times for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
       <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
       <listitem>
@@ -530,7 +542,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +582,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>archive</literal> is written to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T archive:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git src/bin/pg_basebackup/pg_basebackup.c src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..e3770cb 100644
--- src/bin/pg_basebackup/pg_basebackup.c
+++ src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH + 1];
+	char new_dir[MAXPGPATH + 1];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +102,54 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static bool tablespace_dirs_append(char *arg);
+
+/*
+ * Split tablespace argument into old_dir and new_dir, this accounts for
+ * directory name containing a colon.
+ */
+static bool
+tablespace_list_append(char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst = cell->old_dir;
+	const char	*dst_head = dst;
+	const char	*arg_head = arg;
+
+	cell->next = NULL;
+
+	for (; *arg; arg++)
+	{
+		/* Check for overflow */
+		if (dst - dst_head >= MAXPGPATH)
+			return false;
+
+		/* Split on colon not trailing a slash */
+		if (*arg == '\\' && *(arg + 1) == ':')
+			;
+		else if (*arg != ':' || (arg != arg_head && *(arg - 1) == '\\'))
+			*(dst++) = *arg;
+		else if (!*(cell->old_dir) || *(cell->new_dir) || *(arg + 1) != '/')
+			return false;
+		else
+			dst_head = dst = cell->new_dir;
+	}
+
+	if (!(*(cell->old_dir) && *(cell->new_dir)))
+		return false;
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -110,6 +174,8 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions controlling the output:\n"));
 	printf(_("  -D, --pgdata=DIRECTORY receive base backup into directory\n"));
+	printf(_("  -T, --tablespace=OLDDIR:NEWDIR\n"
+			 "                         relocate tablespace olddir to newdir\n"));
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
@@ -861,14 +927,56 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir:new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+
+	return dir;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char linkloc[MAXPGPATH];
+		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +992,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1573,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1618,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1655,6 +1782,7 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
+		{"tablespace", required_argument, NULL, 'T'},
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
@@ -1697,7 +1825,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1705,6 +1833,15 @@ main(int argc, char **argv)
 			case 'D':
 				basedir = pg_strdup(optarg);
 				break;
+			case 'T':
+				if (!tablespace_list_append(optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace format \"%s\", must be \"old_dir:/absolute-path\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'F':
 				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
 					format = 'p';
#14Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Steeve Lennmark (#13)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On Mon, Jan 13, 2014 at 6:13 AM, Steeve Lennmark
<steevel@handeldsbanken.se>wrote:

On Mon, Jan 13, 2014 at 4:29 AM, Andreas Karlsson <andreas@proxel.se>wrote:

On 01/09/2014 10:10 PM, Steeve Lennmark wrote:

That's a much better solution, I attached a patch with the updated code.

# SELECT oid, pg_tablespace_location(oid) FROM pg_tablespace;
[...]
16388 | /tmp/tblspc1
16389 | /tmp/tblspc2

$ pg_basebackup -Xs -D backup/data -T /tmp/tblspc1:$(pwd)/backup/t1 -T
/tmp/tblspc2:$(pwd)/backup/t2

This produces the following now:
$ ls backup/; ls -l backup/data/pg_tblspc/
data t1 t2
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16388 ->
/home/steevel/backup/t1
lrwxrwxrwx 1 steevel users 23 Jan 9 20:41 16389 ->
/home/steevel/backup/t2

Looked at the patch quickly and noticed that it does not support paths
containing colons. Is that an acceptable limitation? The $PATH variable in
most UNIX shells does not support paths with colons either so such naming
of directories is already discouraged.

I thought of this too and wrote a patch for that yesterday, I've
attached an updated version which supports passing in a path with
escaped colons.

Seems I forgot to change the sgml after the syntax change, here's an
updated patch.

--
Steeve Lennmark

Attachments:

0004-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0004-pg_basebackup-relocate-tablespace.patchDownload
commit 5bc533663dcad382ec65bef4a599a17272dfdd0d
Author: Steeve Lennmark <steevel@handeldsbanken.se>
Date:   Thu Jan 9 20:45:26 2014 +0000

    Add support for relocating tablespaces
    
    This is done by mapping old and new tablespace location using the format
    old_dir:new_dir. This also accounts for directory names containing
    colons.

diff --git doc/src/sgml/ref/pg_basebackup.sgml doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..b345154 100644
--- doc/src/sgml/ref/pg_basebackup.sgml
+++ doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,19 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir:newdir</replaceable></option></term>
+      <term><option>--tablespace=<replaceable class="parameter">olddir:newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocates the tablespace in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable>. <replaceable>newdir</replaceable>
+        has to be an absolute path. This options can be specified multiple times
+        for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
       <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
       <listitem>
@@ -530,7 +543,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +583,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>/opt/ts</literal> is relocated to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T /opt/ts:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git src/bin/pg_basebackup/pg_basebackup.c src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..cbc9f87 100644
--- src/bin/pg_basebackup/pg_basebackup.c
+++ src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH];
+	char new_dir[MAXPGPATH];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +102,54 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static bool tablespace_list_append(char *arg);
+
+/*
+ * Split tablespace argument into old_dir and new_dir, this accounts for
+ * directory name containing a colon.
+ */
+static bool
+tablespace_list_append(char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst = cell->old_dir;
+	const char	*dst_head = dst;
+	const char	*arg_head = arg;
+
+	cell->next = NULL;
+
+	for (; *arg; arg++)
+	{
+		/* Check for overflow */
+		if (dst - dst_head >= MAXPGPATH)
+			return false;
+
+		/* Split on colon not trailing a slash */
+		if (*arg == '\\' && *(arg + 1) == ':')
+			;
+		else if (*arg != ':' || (arg != arg_head && *(arg - 1) == '\\'))
+			*dst++ = *arg;
+		else if (!*cell->old_dir || *cell->new_dir || *(arg + 1) != '/')
+			return false;
+		else
+			dst_head = dst = cell->new_dir;
+	}
+
+	if (!(*cell->old_dir && *cell->new_dir))
+		return false;
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -110,6 +174,8 @@ usage(void)
 	printf(_("  %s [OPTION]...\n"), progname);
 	printf(_("\nOptions controlling the output:\n"));
 	printf(_("  -D, --pgdata=DIRECTORY receive base backup into directory\n"));
+	printf(_("  -T, --tablespace=OLDDIR:NEWDIR\n"
+			 "                         relocate tablespace olddir to newdir\n"));
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
@@ -861,14 +927,56 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir:new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+
+	return dir;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char linkloc[MAXPGPATH];
+		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +992,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1573,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1618,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1655,6 +1782,7 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
+		{"tablespace", required_argument, NULL, 'T'},
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
@@ -1697,7 +1825,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1705,6 +1833,15 @@ main(int argc, char **argv)
 			case 'D':
 				basedir = pg_strdup(optarg);
 				break;
+			case 'T':
+				if (!tablespace_list_append(optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace format \"%s\", must be \"old_dir:/absolute-path\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'F':
 				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
 					format = 'p';
#15Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Steeve Lennmark (#14)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Please keep the --help and the options in the SGML table in alphabetical
order within their respective sections.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Steeve Lennmark (#14)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Eyeballing this patch, three thoughts:

1. I wonder whether ilist.c/h should be moved to src/common so that
frontend code could use it.

2. I wonder whether ilist.c should gain the ability to have
singly-linked lists with a pointer to the tail node for appending to the
end. This code would use it, and also the code doing postgresql.conf
parsing which has head/tail pointers all over the place. I'm sure there
are other uses.

3. How many definitions of atooid() do we have now?

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#17Andres Freund
andres@2ndquadrant.com
In reply to: Alvaro Herrera (#16)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Hi,

On 2014-01-16 11:25:59 -0300, Alvaro Herrera wrote:

Eyeballing this patch, three thoughts:

1. I wonder whether ilist.c/h should be moved to src/common so that
frontend code could use it.

Sounds like a good idea. There's some debugging checks that elog, but
that should be fixable easily.

2. I wonder whether ilist.c should gain the ability to have
singly-linked lists with a pointer to the tail node for appending to the
end. This code would use it, and also the code doing postgresql.conf
parsing which has head/tail pointers all over the place. I'm sure there
are other uses.

I am not generaly adverse to it, but neither of those usecases seems to
warrant that. They just should use a doubly linked list, it's not like
the memory/runtime overhead is significant. And the implementation
overhead doesn't count either if they use ilist.h.

Greetings,

Andres Freund

--
Andres Freund http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Alvaro Herrera (#15)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Alvaro,
On Thu, Jan 16, 2014 at 3:20 PM, Alvaro Herrera <alvherre@2ndquadrant.com>wrote:

Please keep the --help and the options in the SGML table in alphabetical
order within their respective sections.

Ah, I failed to see that there was sub groups and thought the options
where not alphabetically ordered. This patch fixes that.

--
Steeve Lennmark

Attachments:

0005-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0005-pg_basebackup-relocate-tablespace.patchDownload
commit c6b714c89f20131ffa642aa019ff1a01b9361672
Author: Steeve Lennmark <steeve@blocket.se>
Date:   Thu Jan 16 17:46:16 2014 +0100

    Add support for relocating tablespaces
    
    This is done by mapping old and new tablespace location using the format
    old_dir:new_dir. This also accounts for directory names containing
    colons.

diff --git doc/src/sgml/ref/pg_basebackup.sgml doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..fb96bd3 100644
--- doc/src/sgml/ref/pg_basebackup.sgml
+++ doc/src/sgml/ref/pg_basebackup.sgml
@@ -203,6 +203,19 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir:newdir</replaceable></option></term>
+      <term><option>--tablespace=<replaceable class="parameter">olddir:newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocates the tablespace in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable>. <replaceable>newdir</replaceable>
+        has to be an absolute path. This options can be specified multiple times
+        for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>--xlogdir=<replaceable class="parameter">xlogdir</replaceable></option></term>
       <listitem>
        <para>
@@ -530,7 +543,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +583,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>/opt/ts</literal> is relocated to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T /opt/ts:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git src/bin/pg_basebackup/pg_basebackup.c src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..a92fa7b 100644
--- src/bin/pg_basebackup/pg_basebackup.c
+++ src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH];
+	char new_dir[MAXPGPATH];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +102,54 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static bool tablespace_list_append(char *arg);
+
+/*
+ * Split tablespace argument into old_dir and new_dir, this accounts for
+ * directory name containing a colon.
+ */
+static bool
+tablespace_list_append(char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst = cell->old_dir;
+	const char	*dst_head = dst;
+	const char	*arg_head = arg;
+
+	cell->next = NULL;
+
+	for (; *arg; arg++)
+	{
+		/* Check for overflow */
+		if (dst - dst_head >= MAXPGPATH)
+			return false;
+
+		/* Split on colon not trailing a slash */
+		if (*arg == '\\' && *(arg + 1) == ':')
+			;
+		else if (*arg != ':' || (arg != arg_head && *(arg - 1) == '\\'))
+			*dst++ = *arg;
+		else if (!*cell->old_dir || *cell->new_dir || *(arg + 1) != '/')
+			return false;
+		else
+			dst_head = dst = cell->new_dir;
+	}
+
+	if (!(*cell->old_dir && *cell->new_dir))
+		return false;
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -113,6 +177,8 @@ usage(void)
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
+	printf(_("  -T, --tablespace=OLDDIR:NEWDIR\n"
+			 "                         relocate tablespace olddir to newdir\n"));
 	printf(_("  -x, --xlog             include required WAL files in backup (fetch mode)\n"));
 	printf(_("  -X, --xlog-method=fetch|stream\n"
 			 "                         include required WAL files with specified method\n"));
@@ -861,14 +927,56 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir:new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+
+	return dir;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char linkloc[MAXPGPATH];
+		snprintf(linkloc, sizeof(linkloc), "%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: unable to remove \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: unable to create symlink \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +992,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1573,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1618,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: not updating pg_tblspc with new tablespace relocation\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1655,6 +1782,7 @@ main(int argc, char **argv)
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
 		{"pgdata", required_argument, NULL, 'D'},
+		{"tablespace", required_argument, NULL, 'T'},
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
@@ -1697,7 +1825,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:T:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1705,6 +1833,15 @@ main(int argc, char **argv)
 			case 'D':
 				basedir = pg_strdup(optarg);
 				break;
+			case 'T':
+				if (!tablespace_list_append(optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace format \"%s\", must be \"old_dir:/absolute-path\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'F':
 				if (strcmp(optarg, "p") == 0 || strcmp(optarg, "plain") == 0)
 					format = 'p';
#19Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Alvaro Herrera (#16)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Alvaro,
On Thu, Jan 16, 2014 at 3:25 PM, Alvaro Herrera <alvherre@2ndquadrant.com>wrote:

Eyeballing this patch, three thoughts:

1. I wonder whether ilist.c/h should be moved to src/common so that
frontend code could use it.

That would be nice, I guess lack of helpers is why a lot of stuff is
using pgdumputils.h from src/bin/pg_dump.

$ git grep -l dumputils.h src/bin/{psql,scripts}
src/bin/psql/command.c
src/bin/psql/copy.c
src/bin/psql/describe.c
src/bin/scripts/clusterdb.c
src/bin/scripts/createdb.c
src/bin/scripts/createuser.c
src/bin/scripts/dropdb.c
src/bin/scripts/dropuser.c
src/bin/scripts/reindexdb.c
src/bin/scripts/vacuumdb.c

3. How many definitions of atooid() do we have now?

$ git grep '#define atooid' |wc -l
11

I found no obvious .h to include though.

--
Steeve Lennmark

#20Peter Eisentraut
peter_e@gmx.net
In reply to: Steeve Lennmark (#18)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

You appear to be generating your patches with git diff --no-prefix.
Don't do that, leave the a/ and b/ in.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Peter Eisentraut
peter_e@gmx.net
In reply to: Steeve Lennmark (#18)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

My review: Clearly, everyone likes this feature.

I'm tempted to think it should be mandatory to specify this option in
plain mode when tablespaces are present. Otherwise, creating a base
backup is liable to create random files all over the place. Obviously,
there would be backward compatibility concerns.

I'm not totally happy with the choice of ":" as the mapping separator,
because that would always require escaping on Windows. We could make it
analogous to the path handling and make ";" the separator on Windows.
Then again, this is not a path, so maybe it should look like one. We
pick something else altogether, like "=".

The option name "--tablespace" isn't very clear. It ought to be
something like "--tablespace-mapping".

I don't think we should require the new directory to be an absolute
path. It should be relative to the current directory, just like the -D
option does it.

I'm not so worried about the atooid() and linked-list duplication. That
can be addressed at some later point.

I would try to write this patch without using MAXPGPATH. I know
existing code uses it, but we should try to use it less, because it
overallocates memory and requires handling additional error conditions.
For example, you catch overflow in tablespace_list_append() but later
report that as invalid tablespace format. We now have psprintf() to
make coding with dynamic memory allocation easier.

It looks like when you ignore an escaped ":" as a separator, you don't
actually unescape it for use as a directory.

OLDDIR and NEWDIR should be capitalized in the help message.

Somehow, I had the subconscious assumption that this feature would do
prefix matching on the directories, not only complete string matching.
So if I had tablespaces in /mnt/data1, /mnt/data2, /mnt/data3, I could
map them all with -T /mnt:mnt and be done. Not sure if that is useful
to many, but it's worth a thought.

Review style guide for error messages:
http://www.postgresql.org/docs/devel/static/error-style-guide.html

We need to think about how to handle this on platforms without symlinks.
I don't like just printing an error message and moving on. It should be
either pass or fail or an option to choose between them.

Please put the options in the getopt handling in alphabetical order.
It's not very alphabetical now, but between D and F is probably not the
best place. ;-)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Peter Eisentraut (#21)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

Peter,
On Thu, Jan 23, 2014 at 2:06 AM, Peter Eisentraut <peter_e@gmx.net> wrote:

I'm tempted to think it should be mandatory to specify this option in
plain mode when tablespaces are present. Otherwise, creating a base
backup is liable to create random files all over the place. Obviously,
there would be backward compatibility concerns.

That was my initial thought too, the one thing that speaks FOR a change
in behaviour is that there isn't a lot of people who have moved over to
pg_basebackup yet and even fewer who use multiple tablespaces. For me
at least pg_basebackup isn't an option at the moment since I use more
than one tablespace.

I'm not totally happy with the choice of ":" as the mapping separator,

because that would always require escaping on Windows. We could make it
analogous to the path handling and make ";" the separator on Windows.
Then again, this is not a path, so maybe it should look like one. We
pick something else altogether, like "=".

The option name "--tablespace" isn't very clear. It ought to be
something like "--tablespace-mapping".

This design choice I made about -T (--tablespace) and colon as
separator was copied from pg_barman, which is the way I back up my
clusters at the moment. Renaming the option to --tablespace-mapping and
changing the syntax to something like "=" is totally fine by me.

I don't think we should require the new directory to be an absolute
path. It should be relative to the current directory, just like the -D
option does it.

Accepting a relative path should be fine, I made a failed attempt using
realpath(3) initially but I guess checking for [0] != '/' and
prepending getcwd(3) would suffice.

I would try to write this patch without using MAXPGPATH. I know

existing code uses it, but we should try to use it less, because it
overallocates memory and requires handling additional error conditions.
For example, you catch overflow in tablespace_list_append() but later
report that as invalid tablespace format. We now have psprintf() to
make coding with dynamic memory allocation easier.

Is overallocating memory in a cli application really an issue though? I
will obviously rewrite the code to fix that if necessary.

It looks like when you ignore an escaped ":" as a separator, you don't

actually unescape it for use as a directory.

+ if (*arg == '\\' && *(arg + 1) == ':')
+ ;

This code handles that case, I could try to make that cleaner.

Somehow, I had the subconscious assumption that this feature would do
prefix matching on the directories, not only complete string matching.
So if I had tablespaces in /mnt/data1, /mnt/data2, /mnt/data3, I could
map them all with -T /mnt:mnt and be done. Not sure if that is useful
to many, but it's worth a thought.

I like that a lot, but I'm afraid the code would have to get a bit more
complex to add that functionality. It would be an easier rewrite if we
added a hint character, something like -T '/mnt/*:mnt'.

Review style guide for error messages:
http://www.postgresql.org/docs/devel/static/error-style-guide.html

I will do that.

We need to think about how to handle this on platforms without symlinks.

I don't like just printing an error message and moving on. It should be
either pass or fail or an option to choose between them.

I hope someone with experience with those kind of systems can come up
with suggestions on how to solve that. I only run postgres on Linux.

Please put the options in the getopt handling in alphabetical order.
It's not very alphabetical now, but between D and F is probably not the
best place. ;-)

Done.

//Steeve

#23Steeve Lennmark
steevel@handeldsbanken.se
In reply to: Steeve Lennmark (#22)
1 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

New patch attached with the following changes:

On Thu, Jan 23, 2014 at 11:01 AM, Steeve Lennmark <steevel@handeldsbanken.se

wrote:

On Thu, Jan 23, 2014 at 2:06 AM, Peter Eisentraut <peter_e@gmx.net> wrote:

I'm not totally happy with the choice of ":" as the mapping separator,
because that would always require escaping on Windows. We could make it
analogous to the path handling and make ";" the separator on Windows.
Then again, this is not a path, so maybe it should look like one. We
pick something else altogether, like "=".

The option name "--tablespace" isn't very clear. It ought to be
something like "--tablespace-mapping".

This design choice I made about -T (--tablespace) and colon as
separator was copied from pg_barman, which is the way I back up my
clusters at the moment. Renaming the option to --tablespace-mapping and
changing the syntax to something like "=" is totally fine by me.

I changed the directory separator from ":" to "=" but made it
configurable in the code.

I don't think we should require the new directory to be an absolute

path. It should be relative to the current directory, just like the -D
option does it.

Accepting a relative path should be fine, I made a failed attempt using
realpath(3) initially but I guess checking for [0] != '/' and
prepending getcwd(3) would suffice.

Relative paths are now accepted. This code will probably not work on
windows though. I tried setting up Windows 8 with the git version of
postgres but was unsuccessful, so I can't really test any of this.
Help on this subject (Windows) would be much appreciated.

Somehow, I had the subconscious assumption that this feature would do

prefix matching on the directories, not only complete string matching.
So if I had tablespaces in /mnt/data1, /mnt/data2, /mnt/data3, I could
map them all with -T /mnt:mnt and be done. Not sure if that is useful
to many, but it's worth a thought.

I like that a lot, but I'm afraid the code would have to get a bit more
complex to add that functionality. It would be an easier rewrite if we
added a hint character, something like -T '/mnt/*:mnt'.

This is not implemented as suggested by Peter in his previous comment.
-T /mnt:mnt now relocates all tablespaces under /mnt to the relative
path mnt.

Review style guide for error messages:

http://www.postgresql.org/docs/devel/static/error-style-guide.html

I've updated error messages according to the style guide.

We need to think about how to handle this on platforms without symlinks.

I don't like just printing an error message and moving on. It should be
either pass or fail or an option to choose between them.

I hope someone with experience with those kind of systems can come up
with suggestions on how to solve that. I only run postgres on Linux.

I would still love some input on this.

Please put the options in the getopt handling in alphabetical order.

It's not very alphabetical now, but between D and F is probably not the
best place. ;-)

Done.

//Steeve

Attachments:

0006-pg_basebackup-relocate-tablespace.patchapplication/octet-stream; name=0006-pg_basebackup-relocate-tablespace.patchDownload
commit fb04753d6554f9450106c9b00047dcd70bb7d068
Author: Steeve Lennmark <steevel@handeldsbanken.se>
Date:   Thu Jan 9 20:45:26 2014 +0000

    Add support for relocating tablespaces
    
    Relocate tablespace(s) by specifying one or more -T olddir=newdir. This
    supports partial matching path matching which makes it possible to
    relocate multiple tablespaces with just one parameter.
    
    Examples:
    -T /tablespaces=backup/tablespaces
    -T /tablespaces/index=backup/tablespaces/index

diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..0555531 100644
--- a/doc/src/sgml/ref/pg_basebackup.sgml
+++ b/doc/src/sgml/ref/pg_basebackup.sgml
@@ -138,6 +138,18 @@ PostgreSQL documentation
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir=newdir</replaceable></option></term>
+      <term><option>--tablespace-mapping=<replaceable class="parameter">olddir=newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocates the tablespace(s) in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable>. This options can be specified multiple times
+        for multiple tablespaces.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>-F <replaceable class="parameter">format</replaceable></option></term>
       <term><option>--format=<replaceable class="parameter">format</replaceable></option></term>
       <listitem>
@@ -530,7 +542,7 @@ PostgreSQL documentation
   <para>
    The way <productname>PostgreSQL</productname> manages tablespaces, the path
    for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   restored, if <replaceable>--tablespace-mapping</replaceable> isn't specified.
   </para>
 
   <para>
@@ -570,6 +582,14 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a two-tablespace local database where tablespace
+   <literal>/opt/ts</literal> is relocated to <literal>./backup/archive</literal>
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D $(pwd)/backup/data -T /opt/ts:$(pwd)/backup/archive</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 9d13d57..8f852b2 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -33,8 +33,27 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+/* Char used to separate olddir and newdir for tablespace */
+static const char TBLSPC_SEP = '=';
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH];
+	char new_dir[MAXPGPATH];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -86,6 +105,74 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_dir(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static bool tablespace_list_append(char *arg);
+
+/*
+ * Split tablespace argument into old_dir and new_dir, this accounts for
+ * directory name containing a colon.
+ */
+static bool
+tablespace_list_append(char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst = cell->old_dir;
+	const char	*dst_head = dst;
+	const char	*arg_head = arg;
+
+	cell->next = NULL;
+
+	for (; *arg; arg++)
+	{
+		/* Check for overflow */
+		if (dst - dst_head >= MAXPGPATH)
+		{
+			fprintf(stderr, _("%s: directory name too long (max %d bytes)\n"),
+					progname, MAXPGPATH);
+			exit(1);
+		}
+
+		/* Split on colon not trailing a slash */
+		if (*arg == '\\' && *(arg + 1) == TBLSPC_SEP)
+			;
+		else if (*arg != TBLSPC_SEP || (arg != arg_head && *(arg - 1) == '\\'))
+			*dst++ = *arg;
+		else if (!*cell->old_dir || *cell->new_dir)
+			return false;
+		/* Found directory separator, switch to new directory */
+		else
+		{
+			dst_head = dst = cell->new_dir;
+
+			/* Add cwd if this is a relative path */
+			if (*(arg + 1) != '/')
+			{
+				if (!getcwd(dst, MAXPGPATH))
+				{
+					fprintf(stderr, _("%s: could not identify current directory: %s\n"),
+							progname, strerror(errno));
+					exit(1);
+				}
+				dst += strlen(dst);
+				*dst++ = '/';
+			}
+		}
+	}
+
+	if (!(*cell->old_dir && *cell->new_dir))
+		return false;
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+
+	return true;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -113,6 +200,8 @@ usage(void)
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
+	printf(_("  -T, --tablespace-mapping=OLDDIR=NEWDIR\n"
+			 "                         relocate tablespace matching olddir to newdir\n"));
 	printf(_("  -x, --xlog             include required WAL files in backup (fetch mode)\n"));
 	printf(_("  -X, --xlog-method=fetch|stream\n"
 			 "                         include required WAL files with specified method\n"));
@@ -861,14 +950,59 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
 }
 
 /*
+ * Retrieve tablespace path, either relocated or original depending on
+ * whether -T old_dir=new_dir was passed or not.
+ */
+static const char *
+get_tablespace_dir(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+		else if (strstr(dir, cell->old_dir) != NULL)
+			return psprintf("%s/%s",
+							cell->new_dir,
+							dir + strlen(cell->old_dir) + 1);
+
+	return dir;
+}
+
+/*
+ * Update symlinks to reflect relocated tablespace, only applied if
+ * tablespace isn't in its original location.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_dir(old_dir);
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char *linkloc = psprintf("%s/pg_tblspc/%d", basedir, oid);
+		if (unlink(linkloc) < 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: could not remove symbolic link \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) < 0)
+		{
+			fprintf(stderr, _("%s: could not create symbolic link \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+/*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
  * symlinks are supported, no other kinds of special files.
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original directory, if tablespace relocation is not enabled.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -884,7 +1018,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_dir(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1465,7 +1599,10 @@ BaseBackup(void)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_dir(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1507,6 +1644,22 @@ BaseBackup(void)
 		progress_report(PQntuples(res), NULL);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+#ifdef HAVE_SYMLINK
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+#else
+		fprintf(stderr, _("%s: tablespace relocation is not supported on this platform\n"),
+				progname);
+#endif
+	}
+
 	PQclear(res);
 
 	/*
@@ -1658,6 +1811,7 @@ main(int argc, char **argv)
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
+		{"tablespace-mapping", required_argument, NULL, 'T'},
 		{"xlog", no_argument, NULL, 'x'},
 		{"xlog-method", required_argument, NULL, 'X'},
 		{"gzip", no_argument, NULL, 'z'},
@@ -1697,7 +1851,7 @@ main(int argc, char **argv)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:F:RT:xX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1721,6 +1875,15 @@ main(int argc, char **argv)
 			case 'R':
 				writerecoveryconf = true;
 				break;
+			case 'T':
+				if (!tablespace_list_append(optarg))
+				{
+					fprintf(stderr,
+							_("%s: invalid tablespace mapping format \"%s\", must be \"olddir=newdir\"\n"),
+							progname, optarg);
+					exit(1);
+				}
+				break;
 			case 'x':
 				if (includewal)
 				{
#24Peter Eisentraut
peter_e@gmx.net
In reply to: Steeve Lennmark (#23)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On 1/29/14, 12:07 PM, Steeve Lennmark wrote:

We need to think about how to handle this on platforms without
symlinks.
I don't like just printing an error message and moving on. It
should be
either pass or fail or an option to choose between them.

I hope someone with experience with those kind of systems can come up
with suggestions on how to solve that. I only run postgres on Linux.

I would still love some input on this.

Currently, pg_basebackup aborts if it has to create a symlink on a
platform that doesn't support it. So your code that updates the
symlinks would never come into play anyway. I'd just update that code
with a "shouldn't get here" comment and add an exit(1).

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#25Peter Eisentraut
peter_e@gmx.net
In reply to: Steeve Lennmark (#23)
2 attachment(s)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

I've been working on your patch. Attached is a version I'd be happy to
commit. Please check that it's okay with you.

I rewrote the option argument parsing logic a little bit to be more
clear and provide more specific error messages.

I reinstated the requirement that both old and new directory are
absolute. After consideration, I think this makes sense because all
tablespace directories are always required to be absolute in other
contexts. (Note: Checking for absolute path by testing the first
character for '/' is not portable.)

I also removed the partial matching. This would have let -T /data1=...
also match /data11, which is clearly confusing. This logic would need
some intelligence about slashes, similar to fnmatch(). This could
perhaps be added later.

Finally, I wrote some test cases for this new functionality. See the
attached patch, which can be applied on top of
<https://commitfest.postgresql.org/action/patch_view?id=1394&gt;.

Attachments:

0001-pg_basebackup-Add-support-for-relocating-tablespaces.patchtext/x-diff; name=0001-pg_basebackup-Add-support-for-relocating-tablespaces.patchDownload
>From cc189020d04ff2311c92108620e4fc74f80c01c9 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Sat, 15 Feb 2014 08:42:20 -0500
Subject: [PATCH] pg_basebackup: Add support for relocating tablespaces

Tablespaces can be relocated in plain backup mode by specifying one or
more -T olddir=newdir options.

From: Steeve Lennmark <steevel@handeldsbanken.se>
Reviewed-by: Peter Eisentraut <peter_e@gmx.net>
---
 doc/src/sgml/ref/pg_basebackup.sgml   |  46 +++++++++-
 src/bin/pg_basebackup/pg_basebackup.c | 166 +++++++++++++++++++++++++++++++++-
 2 files changed, 204 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml
index c379df5..ea22331 100644
--- a/doc/src/sgml/ref/pg_basebackup.sgml
+++ b/doc/src/sgml/ref/pg_basebackup.sgml
@@ -203,6 +203,33 @@ <title>Options</title>
      </varlistentry>
 
      <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir</replaceable>=<replaceable class="parameter">newdir</replaceable></option></term>
+      <term><option>--tablespace-mapping=<replaceable class="parameter">olddir</replaceable>=<replaceable class="parameter">newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocate the tablespace in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable> during the backup.  To be
+        effective, <replaceable>olddir</replaceable> must exactly match the
+        path specification of the tablespace as it is currently defined.  (But
+        it is not an error if there is no tablespace
+        in <replaceable>olddir</replaceable> contained in the backup.)
+        Both <replaceable>olddir</replaceable>
+        and <replaceable>newdir</replaceable> must be absolute paths.  If a
+        path happens to contain a <literal>=</literal> sign, escape it with a
+        backslash.  This option can be specified multiple times for multiple
+        tablespaces.  See examples below.
+       </para>
+
+       <para>
+        If a tablespace is relocated in this way, the symbolic links inside
+        the main data directory are updated to point to the new location.  So
+        the new data directory is ready to be used for a new server instance
+        with all tablespaces in the updated locations.
+        </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><option>--xlogdir=<replaceable class="parameter">xlogdir</replaceable></option></term>
       <listitem>
        <para>
@@ -528,9 +555,13 @@ <title>Notes</title>
   </para>
 
   <para>
-   The way <productname>PostgreSQL</productname> manages tablespaces, the path
-   for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   Tablespaces will in plain format by default be backed up to the same path
+   they have on the server, unless the
+   option <replaceable>--tablespace-mapping</replaceable> is used.  Without
+   this option, running a plain format base backup on the same host as the
+   server will not work if tablespaces are in use, because the backup would
+   have to be written to the same directory locations as the original
+   tablespaces.
   </para>
 
   <para>
@@ -570,6 +601,15 @@ <title>Examples</title>
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a local database where the tablespace in
+   <filename>/opt/ts</filename> is relocated
+   to <filename>./backup/ts</filename>:
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D backup/data -T /opt/ts=$(pwd)/backup/ts</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index b5682d6..b27678f 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -35,8 +35,24 @@
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+	struct TablespaceListCell *next;
+	char old_dir[MAXPGPATH];
+	char new_dir[MAXPGPATH];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+	TablespaceListCell *head;
+	TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char	format = 'p';		/* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -90,6 +106,10 @@
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 					 bool segment_finished);
 
+static const char *get_tablespace_mapping(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static void tablespace_list_append(const char *arg);
+
 
 static void disconnect_and_exit(int code)
 {
@@ -110,6 +130,77 @@ static void disconnect_and_exit(int code)
 }
 
 
+/*
+ * Split argument into old_dir and new_dir and append to tablespace mapping
+ * list.
+ */
+static void
+tablespace_list_append(const char *arg)
+{
+	TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+	char		*dst;
+	char		*dst_ptr;
+	const char	*arg_ptr;
+
+	dst_ptr = dst = cell->old_dir;
+	for (arg_ptr = arg; *arg_ptr; arg_ptr++)
+	{
+		if (dst_ptr - dst >= MAXPGPATH)
+		{
+			fprintf(stderr, _("%s: directory name too long\n"),	progname);
+			exit(1);
+		}
+
+		if (*arg_ptr == '\\' && *(arg_ptr + 1) == '=')
+			;  /* skip backslash escaping = */
+		else if (*arg_ptr == '=' && (arg_ptr == arg || *(arg_ptr - 1) != '\\'))
+		{
+			if (*cell->new_dir)
+			{
+				fprintf(stderr, _("%s: multiple \"=\" signs in tablespace mapping\n"), progname);
+				exit(1);
+			}
+			else
+				dst = dst_ptr = cell->new_dir;
+		}
+		else
+			*dst_ptr++ = *arg_ptr;
+	}
+
+	if (!*cell->old_dir || !*cell->new_dir)
+	{
+		fprintf(stderr,
+				_("%s: invalid tablespace mapping format \"%s\", must be \"OLDDIR=NEWDIR\"\n"),
+				progname, arg);
+		exit(1);
+	}
+
+	/* This check isn't absolutely necessary.  But all tablespaces are created
+	 * with absolute directories, so specifying a non-absolute path here would
+	 * just never match, possibly confusing users.  It's also good to be
+	 * consistent with the new_dir check. */
+	if (!is_absolute_path(cell->old_dir))
+	{
+		fprintf(stderr, _("%s: old directory not absolute in tablespace mapping: %s\n"),
+				progname, cell->old_dir);
+		exit(1);
+	}
+
+	if (!is_absolute_path(cell->new_dir))
+	{
+		fprintf(stderr, _("%s: new directory not absolute in tablespace mapping: %s\n"),
+				progname, cell->new_dir);
+		exit(1);
+	}
+
+	if (tablespace_dirs.tail)
+		tablespace_dirs.tail->next = cell;
+	else
+		tablespace_dirs.head = cell;
+	tablespace_dirs.tail = cell;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -137,6 +228,8 @@ static void disconnect_and_exit(int code)
 	printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
 	printf(_("  -R, --write-recovery-conf\n"
 			 "                         write recovery.conf after backup\n"));
+	printf(_("  -T, --tablespace-mapping=OLDDIR=NEWDIR\n"
+			 "                         relocate tablespace in OLDDIR to NEWDIR\n"));
 	printf(_("  -x, --xlog             include required WAL files in backup (fetch mode)\n"));
 	printf(_("  -X, --xlog-method=fetch|stream\n"
 			 "                         include required WAL files with specified method\n"));
@@ -899,6 +992,52 @@ static void disconnect_and_exit(int code)
 		PQfreemem(copybuf);
 }
 
+
+/*
+ * Retrieve tablespace path, either relocated or original depending on whether
+ * -T was passed or not.
+ */
+static const char *
+get_tablespace_mapping(const char *dir)
+{
+	TablespaceListCell *cell;
+
+	for (cell = tablespace_dirs.head; cell; cell = cell->next)
+		if (strcmp(dir, cell->old_dir) == 0)
+			return cell->new_dir;
+
+	return dir;
+}
+
+
+/*
+ * Update symlinks to reflect relocated tablespace.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+	const char *new_dir = get_tablespace_mapping(old_dir);
+
+	if (strcmp(old_dir, new_dir) != 0)
+	{
+		char *linkloc = psprintf("%s/pg_tblspc/%d", basedir, oid);
+
+		if (unlink(linkloc) != 0 && errno != ENOENT)
+		{
+			fprintf(stderr, _("%s: could not remove symbolic link \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+		if (symlink(new_dir, linkloc) != 0)
+		{
+			fprintf(stderr, _("%s: could not create symbolic link \"%s\": %s"),
+					progname, linkloc, strerror(errno));
+			disconnect_and_exit(1);
+		}
+	}
+}
+
+
 /*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
@@ -906,8 +1045,7 @@ static void disconnect_and_exit(int code)
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original or mapped directory.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -923,7 +1061,7 @@ static void disconnect_and_exit(int code)
 	if (basetablespace)
 		strcpy(current_path, basedir);
 	else
-		strcpy(current_path, PQgetvalue(res, rownum, 1));
+		strcpy(current_path, get_tablespace_mapping(PQgetvalue(res, rownum, 1)));
 
 	/*
 	 * Get the COPY data
@@ -1503,7 +1641,10 @@ static void disconnect_and_exit(int code)
 		 * we do anything anyway.
 		 */
 		if (format == 'p' && !PQgetisnull(res, i, 1))
-			verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+		{
+			char *path = (char *) get_tablespace_mapping(PQgetvalue(res, i, 1));
+			verify_dir_is_empty_or_create(path);
+		}
 	}
 
 	/*
@@ -1545,6 +1686,17 @@ static void disconnect_and_exit(int code)
 		progress_report(PQntuples(res), NULL, true);
 		fprintf(stderr, "\n");	/* Need to move to next line */
 	}
+
+	if (format == 'p' && tablespace_dirs.head != NULL)
+	{
+		for (i = 0; i < PQntuples(res); i++)
+		{
+			Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+			if (tblspc_oid)
+				update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+		}
+	}
+
 	PQclear(res);
 
 	/*
@@ -1696,6 +1848,7 @@ static void disconnect_and_exit(int code)
 		{"format", required_argument, NULL, 'F'},
 		{"checkpoint", required_argument, NULL, 'c'},
 		{"write-recovery-conf", no_argument, NULL, 'R'},
+		{"tablespace-mapping", required_argument, NULL, 'T'},
 		{"xlog", no_argument, NULL, 'x'},
 		{"xlog-method", required_argument, NULL, 'X'},
 		{"gzip", no_argument, NULL, 'z'},
@@ -1735,7 +1888,7 @@ static void disconnect_and_exit(int code)
 		}
 	}
 
-	while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+	while ((c = getopt_long(argc, argv, "D:F:RT:xX:l:zZ:d:c:h:p:U:s:wWvP",
 							long_options, &option_index)) != -1)
 	{
 		switch (c)
@@ -1759,6 +1912,9 @@ static void disconnect_and_exit(int code)
 			case 'R':
 				writerecoveryconf = true;
 				break;
+			case 'T':
+				tablespace_list_append(optarg);
+				break;
 			case 'x':
 				if (includewal)
 				{
-- 
1.8.5.5

pg_basebackup-T-tests.patchtext/x-diff; name=pg_basebackup-T-tests.patchDownload
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index b4c0471..cf7c4b6 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -2,7 +2,7 @@
 use warnings;
 use Cwd;
 use TestLib;
-use Test::More tests => 19;
+use Test::More tests => 31;
 
 program_help_ok('pg_basebackup');
 program_version_ok('pg_basebackup');
@@ -48,6 +48,47 @@
 note("tablespace tars are @tblspc_tars");
 is(scalar(@tblspc_tars), 1, 'one tablespace tar was created');
 
+command_fails(['pg_basebackup', '-D', "$tempdir/backup1", '-Fp'],
+			  'plain format with tablespaces fails without tablespace mapping');
+
+command_ok(['pg_basebackup', '-D', "$tempdir/backup1", '-Fp',
+			"-T$tempdir/tblspc1=$tempdir/tbackup/tblspc1"],
+		   'plain format with tablespaces succeeds with tablespace mapping');
+ok(-d "$tempdir/tbackup/tblspc1", 'tablespace was relocated');
+opendir(my $dh, "$tempdir/pgdata/pg_tblspc") or die;
+ok((grep { -l "$tempdir/backup1/pg_tblspc/$_" and readlink "$tempdir/backup1/pg_tblspc/$_" eq "$tempdir/tbackup/tblspc1" } readdir($dh)),
+   "tablespace symlink was updated");
+closedir $dh;
+
+mkdir "$tempdir/tbl=spc2";
+psql 'postgres', "DROP TABLE test1;";
+psql 'postgres', "DROP TABLESPACE tblspc1;";
+psql 'postgres', "CREATE TABLESPACE tblspc2 LOCATION '$tempdir/tbl=spc2';";
+command_ok(['pg_basebackup', '-D', "$tempdir/backup3", '-Fp',
+			"-T$tempdir/tbl\\=spc2=$tempdir/tbackup/tbl\\=spc2"],
+		   'mapping tablespace with = sign in path');
+ok(-d "$tempdir/tbackup/tbl=spc2", 'tablespace with = sign was relocated');
+
+psql 'postgres', "DROP TABLESPACE tblspc2;";
+
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-T=/foo"],
+			  '-T with empty old directory fails');
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-T/foo="],
+			  '-T with empty new directory fails');
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-T/foo=/bar=/baz"],
+			  '-T with multiple = fails');
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-Tfoo=/bar"],
+			  '-T with old directory not absolute fails');
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-T/foo=bar"],
+			  '-T with new directory not absolute fails');
+command_fails(['pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
+			   "-Tfoo"],
+			  '-T with invalid format fails');
 
 our $TODO = 'https://commitfest.postgresql.org/action/patch_view?id=1303';
 
#26Peter Eisentraut
peter_e@gmx.net
In reply to: Peter Eisentraut (#25)
Re: [PATCH] Relocation of tablespaces in pg_basebackup

On 2/15/14, 7:05 PM, Peter Eisentraut wrote:

I've been working on your patch. Attached is a version I'd be happy to
commit. Please check that it's okay with you.

Committed after some rebasing.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers