From 16540c5e689e8b97345b0dd8ec97451109ea8a87 Mon Sep 17 00:00:00 2001
From: Bruce Toll <btoll@dhsus.com>
Date: Fri, 21 Mar 2014 17:25:57 -0400
Subject: [PATCH] Support postgres TIMESTAMP WITH TIME ZONE.

Enhance dbo postgres backend to support TIMESTAMP WITH TIME ZONE
columns.

This update addresses an issue where writing a WDateTime to a
TIMESTAMP WITH TIME ZONE column could be incorrect, depending on
timezone settings.

It also addresses an issue where attempting to read a WDateTime value
from a TIMESTAMP WITH TIME ZONE column would result in either an
incorrect value or a bad lexical cast.

Wt Dbo creates TIMESTAMP columns by default for WDateTime values.
However, TIMESTAMP WITH TIME ZONE columns might arise with legacy
databases or to facilitate interaction with other software.

Test cases to drive the problem are included. The tests dbo_test_22c and
dbo_test_22d fail prior to applying this update.
---
 src/Wt/Dbo/backend/Postgres.C |  20 ++++++-
 test/dbo/DboTest.C            | 120 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 138 insertions(+), 2 deletions(-)

diff --git a/src/Wt/Dbo/backend/Postgres.C b/src/Wt/Dbo/backend/Postgres.C
index d378fa1..0aa03c0 100644
--- a/src/Wt/Dbo/backend/Postgres.C
+++ b/src/Wt/Dbo/backend/Postgres.C
@@ -140,6 +140,12 @@ public:
     else {
       v = boost::posix_time::to_iso_extended_string(value);
       v[v.find('T')] = ' ';
+      /*
+       * Add explicit timezone offset. Postgres will ignore this for a TIMESTAMP
+       * column, but will treat the timestamp as UTC in a TIMESTAMP WITH TIME ZONE
+       * column -- possibly in a legacy table.
+       */
+      v.append("+00");
     }
 
     setValue(column, v);
@@ -390,8 +396,18 @@ public:
     if (type == SqlDate)
       *value = boost::posix_time::ptime(boost::gregorian::from_string(v),
 					boost::posix_time::hours(0));
-    else
-      *value = boost::posix_time::time_from_string(v);
+    else {
+      /*
+       * Handle timezone offset. Postgres will append a timezone offset [+-]dd
+       * if a column is defined as TIMESTAMP WITH TIME ZONE -- possibly
+       * in a legacy table. If offset is present, subtract it for UTC output.
+       */
+      if (v.size() >= 3 && std::strchr("+-", v[v.size() - 3]))
+        *value = boost::posix_time::time_from_string(v.substr(0, v.size() - 3)) -
+          boost::posix_time::hours(boost::lexical_cast<int>(v.substr(v.size() - 3)));
+      else
+        *value = boost::posix_time::time_from_string(v);
+    }
 
     DEBUG(std::cerr << this 
 	  << " result time_duration " << column << " " << *value << std::endl);
diff --git a/test/dbo/DboTest.C b/test/dbo/DboTest.C
index 65b275c..d623c03 100644
--- a/test/dbo/DboTest.C
+++ b/test/dbo/DboTest.C
@@ -1877,3 +1877,123 @@ BOOST_AUTO_TEST_CASE( dbo_test21 )
     delete model;
   }
 }
+
+BOOST_AUTO_TEST_CASE( dbo_test22a )
+{
+#ifdef POSTGRES
+  DboFixture f;
+  dbo::Session *session_ = f.session_;
+  Wt::WDateTime datetime1 = Wt::WDateTime(Wt::WDate(2009, 10, 1),
+                                          Wt::WTime(12, 11, 31));
+
+  {
+    dbo::Transaction t(*session_);
+    session_->execute("SET TIME ZONE \"America/New_York\"");
+
+    dbo::ptr<A> a1(new A());
+    a1.modify()->datetime = datetime1;
+
+    session_->add(a1);
+    t.commit();
+  }
+
+  {
+    dbo::Transaction t(*session_);
+
+    dbo::ptr<A> a2 = session_->find<A>();
+
+    BOOST_REQUIRE(a2->datetime == datetime1);
+  }
+#endif //POSTGRES
+}
+
+BOOST_AUTO_TEST_CASE( dbo_test22b )
+{
+#ifdef POSTGRES
+  DboFixture f;
+  dbo::Session *session_ = f.session_;
+  Wt::WDateTime datetime1 = Wt::WDateTime(Wt::WDate(2009, 10, 1),
+                                          Wt::WTime(12, 11, 31));
+
+  {
+    dbo::Transaction t(*session_);
+    session_->execute("SET TIME ZONE \"Europe/Brussels\"");
+
+    dbo::ptr<A> a1(new A());
+    a1.modify()->datetime = datetime1;
+
+    session_->add(a1);
+    t.commit();
+  }
+
+  {
+    dbo::Transaction t(*session_);
+
+    dbo::ptr<A> a2 = session_->find<A>();
+
+    BOOST_REQUIRE(a2->datetime == datetime1);
+  }
+#endif //POSTGRES
+}
+
+BOOST_AUTO_TEST_CASE( dbo_test22c )
+{
+#ifdef POSTGRES
+  DboFixture f;
+  dbo::Session *session_ = f.session_;
+  Wt::WDateTime datetime1 = Wt::WDateTime(Wt::WDate(2009, 10, 1),
+                                          Wt::WTime(12, 11, 31));
+
+  {
+    dbo::Transaction t(*session_);
+    session_->execute("ALTER TABLE table_a ALTER COLUMN datetime "
+        "SET DATA TYPE TIMESTAMP WITH TIME ZONE" );
+    session_->execute("SET TIME ZONE \"America/New_York\"");
+
+    dbo::ptr<A> a1(new A());
+    a1.modify()->datetime = datetime1;
+
+    session_->add(a1);
+    t.commit();
+  }
+
+  {
+    dbo::Transaction t(*session_);
+
+    dbo::ptr<A> a2 = session_->find<A>();
+
+    BOOST_REQUIRE(a2->datetime == datetime1);
+  }
+#endif //POSTGRES
+}
+
+BOOST_AUTO_TEST_CASE( dbo_test22d )
+{
+#ifdef POSTGRES
+  DboFixture f;
+  dbo::Session *session_ = f.session_;
+  Wt::WDateTime datetime1 = Wt::WDateTime(Wt::WDate(2009, 10, 1),
+                                          Wt::WTime(12, 11, 31));
+
+  {
+    dbo::Transaction t(*session_);
+    session_->execute("ALTER TABLE table_a ALTER COLUMN datetime "
+        "SET DATA TYPE TIMESTAMP WITH TIME ZONE" );
+    session_->execute("SET TIME ZONE \"Europe/Brussels\"");
+
+    dbo::ptr<A> a1(new A());
+    a1.modify()->datetime = datetime1;
+
+    session_->add(a1);
+    t.commit();
+  }
+
+  {
+    dbo::Transaction t(*session_);
+
+    dbo::ptr<A> a2 = session_->find<A>();
+
+    BOOST_REQUIRE(a2->datetime == datetime1);
+  }
+#endif //POSTGRES
+}
-- 
1.8.5.3

