]> granicus.if.org Git - postgresql/commitdiff
Fix contrib/dblink to handle inconsistent DateStyle/IntervalStyle safely.
authorTom Lane <tgl@sss.pgh.pa.us>
Fri, 22 Mar 2013 19:22:21 +0000 (15:22 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Fri, 22 Mar 2013 19:22:21 +0000 (15:22 -0400)
If the remote database's settings of these GUCs are different from ours,
ambiguous datetime values may be read incorrectly.  To fix, temporarily
adopt the remote server's settings while we ingest a query result.

This is not a complete fix, since it doesn't do anything about ambiguous
values in commands sent to the remote server; but there seems little we
can do about that end of it given dblink's entirely textual API for
transmitted commands.

Back-patch to 9.2.  The hazard exists in all versions, but this patch
would need more work to apply before 9.2.  Given the lack of field
complaints about this issue, it doesn't seem worth the effort at present.

Daniel Farina and Tom Lane

contrib/dblink/dblink.c
contrib/dblink/expected/dblink.out
contrib/dblink/sql/dblink.sql

index a67a836eba1fc5e3ea14701284b831269fbc3357..2cc7c8fefa13956674daee0a2e59a4b2cb1bbaf0 100644 (file)
@@ -47,6 +47,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
+#include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
@@ -80,7 +81,8 @@ typedef struct storeInfo
  */
 static Datum dblink_record_internal(FunctionCallInfo fcinfo, bool is_async);
 static void prepTuplestoreResult(FunctionCallInfo fcinfo);
-static void materializeResult(FunctionCallInfo fcinfo, PGresult *res);
+static void materializeResult(FunctionCallInfo fcinfo, PGconn *conn,
+                                 PGresult *res);
 static void materializeQueryResult(FunctionCallInfo fcinfo,
                                           PGconn *conn,
                                           const char *conname,
@@ -110,6 +112,8 @@ static char *escape_param_str(const char *from);
 static void validate_pkattnums(Relation rel,
                                   int2vector *pkattnums_arg, int32 pknumatts_arg,
                                   int **pkattnums, int *pknumatts);
+static int     applyRemoteGucs(PGconn *conn);
+static void restoreLocalGucs(int nestlevel);
 
 /* Global */
 static remoteConn *pconn = NULL;
@@ -597,7 +601,7 @@ dblink_fetch(PG_FUNCTION_ARGS)
                                 errmsg("cursor \"%s\" does not exist", curname)));
        }
 
-       materializeResult(fcinfo, res);
+       materializeResult(fcinfo, conn, res);
        return (Datum) 0;
 }
 
@@ -742,7 +746,7 @@ dblink_record_internal(FunctionCallInfo fcinfo, bool is_async)
                                }
                                else
                                {
-                                       materializeResult(fcinfo, res);
+                                       materializeResult(fcinfo, conn, res);
                                }
                        }
                }
@@ -798,7 +802,7 @@ prepTuplestoreResult(FunctionCallInfo fcinfo)
  * The PGresult will be released in this function.
  */
 static void
-materializeResult(FunctionCallInfo fcinfo, PGresult *res)
+materializeResult(FunctionCallInfo fcinfo, PGconn *conn, PGresult *res)
 {
        ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 
@@ -808,7 +812,7 @@ materializeResult(FunctionCallInfo fcinfo, PGresult *res)
        PG_TRY();
        {
                TupleDesc       tupdesc;
-               bool            is_sql_cmd = false;
+               bool            is_sql_cmd;
                int                     ntuples;
                int                     nfields;
 
@@ -869,6 +873,7 @@ materializeResult(FunctionCallInfo fcinfo, PGresult *res)
                if (ntuples > 0)
                {
                        AttInMetadata *attinmeta;
+                       int                     nestlevel = -1;
                        Tuplestorestate *tupstore;
                        MemoryContext oldcontext;
                        int                     row;
@@ -876,6 +881,10 @@ materializeResult(FunctionCallInfo fcinfo, PGresult *res)
 
                        attinmeta = TupleDescGetAttInMetadata(tupdesc);
 
+                       /* Set GUCs to ensure we read GUC-sensitive data types correctly */
+                       if (!is_sql_cmd)
+                               nestlevel = applyRemoteGucs(conn);
+
                        oldcontext = MemoryContextSwitchTo(
                                                                        rsinfo->econtext->ecxt_per_query_memory);
                        tupstore = tuplestore_begin_heap(true, false, work_mem);
@@ -912,6 +921,9 @@ materializeResult(FunctionCallInfo fcinfo, PGresult *res)
                                tuplestore_puttuple(tupstore, tuple);
                        }
 
+                       /* clean up GUC settings, if we changed any */
+                       restoreLocalGucs(nestlevel);
+
                        /* clean up and return the tuplestore */
                        tuplestore_donestoring(tupstore);
                }
@@ -1045,6 +1057,7 @@ static PGresult *
 storeQueryResult(storeInfo *sinfo, PGconn *conn, const char *sql)
 {
        bool            first = true;
+       int                     nestlevel = -1;
        PGresult   *res;
 
        if (!PQsendQuery(conn, sql))
@@ -1064,6 +1077,15 @@ storeQueryResult(storeInfo *sinfo, PGconn *conn, const char *sql)
                if (PQresultStatus(sinfo->cur_res) == PGRES_SINGLE_TUPLE)
                {
                        /* got one row from possibly-bigger resultset */
+
+                       /*
+                        * Set GUCs to ensure we read GUC-sensitive data types correctly.
+                        * We shouldn't do this until we have a row in hand, to ensure
+                        * libpq has seen any earlier ParameterStatus protocol messages.
+                        */
+                       if (first && nestlevel < 0)
+                               nestlevel = applyRemoteGucs(conn);
+
                        storeRow(sinfo, sinfo->cur_res, first);
 
                        PQclear(sinfo->cur_res);
@@ -1084,6 +1106,9 @@ storeQueryResult(storeInfo *sinfo, PGconn *conn, const char *sql)
                }
        }
 
+       /* clean up GUC settings, if we changed any */
+       restoreLocalGucs(nestlevel);
+
        /* return last_res */
        res = sinfo->last_res;
        sinfo->last_res = NULL;
@@ -2765,3 +2790,73 @@ validate_pkattnums(Relation rel,
                                         errmsg("invalid attribute number %d", pkattnum)));
        }
 }
+
+/*
+ * Copy the remote session's values of GUCs that affect datatype I/O
+ * and apply them locally in a new GUC nesting level.  Returns the new
+ * nestlevel (which is needed by restoreLocalGucs to undo the settings),
+ * or -1 if no new nestlevel was needed.
+ *
+ * We use the equivalent of a function SET option to allow the settings to
+ * persist only until the caller calls restoreLocalGucs.  If an error is
+ * thrown in between, guc.c will take care of undoing the settings.
+ */
+static int
+applyRemoteGucs(PGconn *conn)
+{
+       static const char *const GUCsAffectingIO[] = {
+               "DateStyle",
+               "IntervalStyle"
+       };
+
+       int                     nestlevel = -1;
+       int                     i;
+
+       for (i = 0; i < lengthof(GUCsAffectingIO); i++)
+       {
+               const char *gucName = GUCsAffectingIO[i];
+               const char *remoteVal = PQparameterStatus(conn, gucName);
+               const char *localVal;
+
+               /*
+                * If the remote server is pre-8.4, it won't have IntervalStyle, but
+                * that's okay because its output format won't be ambiguous.  So just
+                * skip the GUC if we don't get a value for it.  (We might eventually
+                * need more complicated logic with remote-version checks here.)
+                */
+               if (remoteVal == NULL)
+                       continue;
+
+               /*
+                * Avoid GUC-setting overhead if the remote and local GUCs already
+                * have the same value.
+                */
+               localVal = GetConfigOption(gucName, false, false);
+               Assert(localVal != NULL);
+
+               if (strcmp(remoteVal, localVal) == 0)
+                       continue;
+
+               /* Create new GUC nest level if we didn't already */
+               if (nestlevel < 0)
+                       nestlevel = NewGUCNestLevel();
+
+               /* Apply the option (this will throw error on failure) */
+               (void) set_config_option(gucName, remoteVal,
+                                                                PGC_USERSET, PGC_S_SESSION,
+                                                                GUC_ACTION_SAVE, true, 0);
+       }
+
+       return nestlevel;
+}
+
+/*
+ * Restore local GUCs after they have been overlaid with remote settings.
+ */
+static void
+restoreLocalGucs(int nestlevel)
+{
+       /* Do nothing if no new nestlevel was created */
+       if (nestlevel > 0)
+               AtEOXact_GUC(true, nestlevel);
+}
index 1db153aeb1af861d776530d44b43b7d1ea05b4f9..8f07cb06cd12f457f6caebfa4d97674a94155de0 100644 (file)
@@ -914,3 +914,179 @@ SELECT dblink_build_sql_delete('test_dropped', '1', 1,
  DELETE FROM test_dropped WHERE id = '2'
 (1 row)
 
+-- test local mimicry of remote GUC values that affect datatype I/O
+SET datestyle = ISO, MDY;
+SET intervalstyle = postgres;
+SET timezone = UTC;
+SELECT dblink_connect('myconn','dbname=contrib_regression');
+ dblink_connect 
+----------------
+ OK
+(1 row)
+
+SELECT dblink_exec('myconn', 'SET datestyle = GERMAN, DMY;');
+ dblink_exec 
+-------------
+ SET
+(1 row)
+
+-- single row synchronous case
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM (VALUES (''12.03.2013 00:00:00+00'')) t')
+  AS t(a timestamptz);
+           a            
+------------------------
+ 2013-03-12 00:00:00+00
+(1 row)
+
+-- multi-row synchronous case
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00''),
+             (''12.03.2013 00:00:00+00'')) t')
+  AS t(a timestamptz);
+           a            
+------------------------
+ 2013-03-12 00:00:00+00
+ 2013-03-12 00:00:00+00
+(2 rows)
+
+-- single-row asynchronous case
+SELECT *
+FROM dblink_send_query('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00'')) t');
+ dblink_send_query 
+-------------------
+                 1
+(1 row)
+
+CREATE TEMPORARY TABLE result AS
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz));
+SELECT * FROM result;
+           t            
+------------------------
+ 2013-03-12 00:00:00+00
+(1 row)
+
+DROP TABLE result;
+-- multi-row asynchronous case
+SELECT *
+FROM dblink_send_query('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00''),
+             (''12.03.2013 00:00:00+00'')) t');
+ dblink_send_query 
+-------------------
+                 1
+(1 row)
+
+CREATE TEMPORARY TABLE result AS
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz));
+SELECT * FROM result;
+           t            
+------------------------
+ 2013-03-12 00:00:00+00
+ 2013-03-12 00:00:00+00
+(2 rows)
+
+DROP TABLE result;
+-- Try an ambiguous interval
+SELECT dblink_exec('myconn', 'SET intervalstyle = sql_standard;');
+ dblink_exec 
+-------------
+ SET
+(1 row)
+
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM (VALUES (''-1 2:03:04'')) i')
+  AS i(i interval);
+         i         
+-------------------
+ -1 days -02:03:04
+(1 row)
+
+-- Try swapping to another format to ensure the GUCs are tracked
+-- properly through a change.
+CREATE TEMPORARY TABLE result (t timestamptz);
+SELECT dblink_exec('myconn', 'SET datestyle = ISO, MDY;');
+ dblink_exec 
+-------------
+ SET
+(1 row)
+
+INSERT INTO result
+  SELECT *
+  FROM dblink('myconn',
+              'SELECT * FROM (VALUES (''03.12.2013 00:00:00+00'')) t')
+    AS t(a timestamptz);
+SELECT dblink_exec('myconn', 'SET datestyle = GERMAN, DMY;');
+ dblink_exec 
+-------------
+ SET
+(1 row)
+
+INSERT INTO result
+  SELECT *
+  FROM dblink('myconn',
+              'SELECT * FROM (VALUES (''12.03.2013 00:00:00+00'')) t')
+    AS t(a timestamptz);
+SELECT * FROM result;
+           t            
+------------------------
+ 2013-03-12 00:00:00+00
+ 2013-03-12 00:00:00+00
+(2 rows)
+
+DROP TABLE result;
+-- Check error throwing in dblink_fetch
+SELECT dblink_open('myconn','error_cursor',
+       'SELECT * FROM (VALUES (''1''), (''not an int'')) AS t(text);');
+ dblink_open 
+-------------
+ OK
+(1 row)
+
+SELECT *
+FROM dblink_fetch('myconn','error_cursor', 1) AS t(i int);
+ i 
+---
+ 1
+(1 row)
+
+SELECT *
+FROM dblink_fetch('myconn','error_cursor', 1) AS t(i int);
+ERROR:  invalid input syntax for integer: "not an int"
+-- Make sure that the local settings have retained their values in spite
+-- of shenanigans on the connection.
+SHOW datestyle;
+ DateStyle 
+-----------
+ ISO, MDY
+(1 row)
+
+SHOW intervalstyle;
+ IntervalStyle 
+---------------
+ postgres
+(1 row)
+
+-- Clean up GUC-setting tests
+SELECT dblink_disconnect('myconn');
+ dblink_disconnect 
+-------------------
+ OK
+(1 row)
+
+RESET datestyle;
+RESET intervalstyle;
+RESET timezone;
index 8c8ffe233cfc0147b1f168bada925fbe12a56b52..245f50e97b93090ed656f31460b4ba6b39a5cb74 100644 (file)
@@ -425,3 +425,99 @@ SELECT dblink_build_sql_update('test_dropped', '1', 1,
 
 SELECT dblink_build_sql_delete('test_dropped', '1', 1,
                                ARRAY['2'::TEXT]);
+
+-- test local mimicry of remote GUC values that affect datatype I/O
+SET datestyle = ISO, MDY;
+SET intervalstyle = postgres;
+SET timezone = UTC;
+SELECT dblink_connect('myconn','dbname=contrib_regression');
+SELECT dblink_exec('myconn', 'SET datestyle = GERMAN, DMY;');
+
+-- single row synchronous case
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM (VALUES (''12.03.2013 00:00:00+00'')) t')
+  AS t(a timestamptz);
+
+-- multi-row synchronous case
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00''),
+             (''12.03.2013 00:00:00+00'')) t')
+  AS t(a timestamptz);
+
+-- single-row asynchronous case
+SELECT *
+FROM dblink_send_query('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00'')) t');
+CREATE TEMPORARY TABLE result AS
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz));
+SELECT * FROM result;
+DROP TABLE result;
+
+-- multi-row asynchronous case
+SELECT *
+FROM dblink_send_query('myconn',
+    'SELECT * FROM
+     (VALUES (''12.03.2013 00:00:00+00''),
+             (''12.03.2013 00:00:00+00'')) t');
+CREATE TEMPORARY TABLE result AS
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz))
+UNION ALL
+(SELECT * from dblink_get_result('myconn') as t(t timestamptz));
+SELECT * FROM result;
+DROP TABLE result;
+
+-- Try an ambiguous interval
+SELECT dblink_exec('myconn', 'SET intervalstyle = sql_standard;');
+SELECT *
+FROM dblink('myconn',
+    'SELECT * FROM (VALUES (''-1 2:03:04'')) i')
+  AS i(i interval);
+
+-- Try swapping to another format to ensure the GUCs are tracked
+-- properly through a change.
+CREATE TEMPORARY TABLE result (t timestamptz);
+
+SELECT dblink_exec('myconn', 'SET datestyle = ISO, MDY;');
+INSERT INTO result
+  SELECT *
+  FROM dblink('myconn',
+              'SELECT * FROM (VALUES (''03.12.2013 00:00:00+00'')) t')
+    AS t(a timestamptz);
+
+SELECT dblink_exec('myconn', 'SET datestyle = GERMAN, DMY;');
+INSERT INTO result
+  SELECT *
+  FROM dblink('myconn',
+              'SELECT * FROM (VALUES (''12.03.2013 00:00:00+00'')) t')
+    AS t(a timestamptz);
+
+SELECT * FROM result;
+
+DROP TABLE result;
+
+-- Check error throwing in dblink_fetch
+SELECT dblink_open('myconn','error_cursor',
+       'SELECT * FROM (VALUES (''1''), (''not an int'')) AS t(text);');
+SELECT *
+FROM dblink_fetch('myconn','error_cursor', 1) AS t(i int);
+SELECT *
+FROM dblink_fetch('myconn','error_cursor', 1) AS t(i int);
+
+-- Make sure that the local settings have retained their values in spite
+-- of shenanigans on the connection.
+SHOW datestyle;
+SHOW intervalstyle;
+
+-- Clean up GUC-setting tests
+SELECT dblink_disconnect('myconn');
+RESET datestyle;
+RESET intervalstyle;
+RESET timezone;