? ChangeLog
Index: date_api.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/date/date_api.module,v
retrieving revision 1.46
diff -u -3 -p -r1.46 date_api.module
--- date_api.module	1 Feb 2008 11:48:54 -0000	1.46
+++ date_api.module	12 Feb 2008 05:46:13 -0000
@@ -780,9 +780,10 @@ function date_diff($date1_in, $date2_in,
  * @param string $to_type
  *   the type of date to convert to
  */
-function date_convert($date, $from_type, $to_type) {
+function date_convert($date, $from_type, $to_type, $from_timezone = NULL) {
   if (empty($date) && !$date === 0) return NULL;
   if (empty($from_type) || empty($to_type) || $from_type == $to_type) return $date;
+  if (empty($from_timezone)) $from_timezone = 'UTC';
   switch ($from_type) {
     case DATE_OBJECT:
       if (!is_object($date)) return NULL;
@@ -792,24 +793,24 @@ function date_convert($date, $from_type,
     case DATE_ISO:
       if (!preg_match(DATE_REGEX_LOOSE, $date)) return NULL;
       $date = str_replace('T', ' ', $date);
-      $obj = date_make_date($date, 'UTC');
+      $obj = date_make_date($date, $from_timezone);
       break;
     case DATE_ICAL:
       if (!preg_match(DATE_REGEX_LOOSE, $date)) return NULL;
       preg_match(DATE_REGEX_LOOSE, $date, $regs);
       $datetime = date_pad($regs[1], 4) .'-'. date_pad($regs[2]) .'-'. date_pad($regs[3]) .
         'T'. date_pad($regs[5]) .':'. date_pad($regs[6]) .':'. date_pad($regs[7]);
-      $obj = date_make_date($datetime, 'UTC');
+      $obj = date_make_date($datetime, $from_timezone);
     case DATE_UNIX:
       if (!is_numeric($date)) return NULL;
-      $obj = date_make_date($date, 'UTC', DATE_UNIX);
+      $obj = date_make_date($date, $from_timezone, DATE_UNIX);
       break;
     case DATE_ARRAY:
       if (!is_array($date)) return NULL;
       $datetime = date_pad(intval($date['year']), 4) .'-'. date_pad(intval($date['month'])) .
             '-'. date_pad(intval($date['day'])) .' '. date_pad(intval($date['hour'])) .
             ':'. date_pad(intval($date['minute'])) .':'. date_pad(intval($date['second']));
-      $obj = date_make_date($datetime, 'UTC');
+      $obj = date_make_date($datetime, $from_timezone);
       break;
   }
   switch ($to_type) {
Index: date_api_sql.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/date/date_api_sql.inc,v
retrieving revision 1.6
diff -u -3 -p -r1.6 date_api_sql.inc
--- date_api_sql.inc	8 Feb 2008 20:12:55 -0000	1.6
+++ date_api_sql.inc	12 Feb 2008 05:46:14 -0000
@@ -5,127 +5,284 @@
  * Helper function to test if the database is set up to do timezone
  * conversions natively.
  */
-function date_sql_db_timezone_support() {
+function date_sql_db_timezone_support($uses_date_db_sql_timezone = FALSE) {
   global $db_type;
 
-  switch ($db_type) {
-    case 'mysql':
-    case 'mysqli':
-      // Make sure the function returns a valid value and the data is current.
-      $test = db_result(db_query("SELECT CONVERT_TZ('2007-03-11 2:00:00','US/Eastern','US/Central')"));
-      if ($test == '2007-03-11 01:00:00') {
-        return TRUE;
-      }
-      return FALSE;
+  if (!$uses_date_db_sql_timezone && $db_type == 'pgsql') {
+    // pgsql requires joins for proper functionality.
+    return FALSE;
+  }
 
-    case 'postgres':
-      // TODO, need to find a way to test this for sure in postgres, but it
-      // seems to be the default situation.
-      return TRUE;
+  $tz_eastern = date_db_sql_timezone('US/Eastern');
+  $tz_central = date_db_sql_timezone('US/Central');
+  $test_query = date_db_sql('DATE', "'2007-03-11 2:00:00'", $tz_eastern, $tz_central);
+  $test = db_result(db_query("SELECT $test_query $tz_eastern[1] $tz_central[1]"));
+  if ($test == '2007-03-11 01:00:00') {
+    return TRUE;
   }
+  return FALSE;
 }
 
 /**
- * Cross-database date SQL wrapper function for databases that support
- * native timezone handling, allows use of normalized native date
- * functions in both mysql and postgres.
+ * Cross-database date SQL wrapper function for dates.  For databases that
+ * support native timezone handling, allows use of normalized native date
+ * in both mysql and postgres.
  *
  * Designed to be extensible to other databases.
  *
+ * Use of time zones requires time zone support in the SQL server, so leave
+ * both NULL if you don't have a need to query multiple times according to
+ * a time zone stored in the database.
+ *
+ * Time zones must be formatted with date_db_sql_timezone() or be an array
+ * with the timezone field in it.
+ *
+ * For compatibility with the non-UTC Date API, strings are also accepted, but only work
+ * correctly with mysql databases because of the joins.  This usage is not recommended.
+ *
+ * This function *DOES NOT* append the join part of the zone formatted with
+ * date_db_sql_timezone(), so you must do it yourself.
+ *
  * @param $result_type - NOW, DATE, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW, DOY, WEEK
  * @param $field - the name of the date field to be analyzed
- * @param $timezone_from - the timezone that the date in the db is stored in.
+ * @param $timezone_from - the timezone that the date in the db is stored in
  * @param $timezone_to - the name of the timezone to convert the date to
  * @param $date_type - the type of date field being analyzed, DATE_DATETIME, DATE_UNIX or DATE_ISO
  * @return a SQL statement appropriate for the $db_type
  *
- * $timezone_from and $timezone_to can be either a string name of a timezone
- * or the field name of a field that contains the timezone name. To make the
- * code work either way, string names must be prefixed and suffixed with single
- * quotes, i.e. "'UTD'".
- *
  * example:
- * date_db_sql('WEEK', 'MYFIELD', 'UTC', 'US/Central', DATE_ISO)
+ * date_db_sql('WEEK', 'MYFIELD', NULL, NULL, DATE_ISO)
  *
  * mysql returns:
- * WEEK(CONVERT_TZ(STR_TO_DATE(MYFIELD, '%Y-%m-%dT%T), 'UTC', 'US/Central'), 3)
+ * WEEK(CAST(MYFIELD AS DATETIME), 3)
  *
  * postgres returns:
- * EXTRACT(WEEK FROM((SELECT TIMESTAMP WITH TIME ZONE 'TO_DATE(MYFIELD, 'FMYYYY-FMMM-FMDDTFMHH:FMMI:FMSS') UTC' AT TIME ZONE 'US/Central')))
+ * EXTRACT(WEEK FROM(CAST(MYFIELD AS TIMESTAMP)))
  */
-function date_db_sql($result_type, $field, $timezone_from = "'UTC'", $timezone_to = NULL, $date_type = DATE_DATETIME) {
+function date_db_sql($result_type, $field, $timezone_from = NULL, $timezone_to = NULL, $date_type = DATE_DATETIME) {
   global $db_type;
-  if (empty($timezone_to)) {
-    $timezone_to = "'". date_default_timezone_name() ."'";
-  }
 
-  // NOW() is timezone-adjusted by db to the server timezone,
-  // re-adjust to the desired timezone.
-  if ($date_type == 'NOW' || $field == 'NOW()') {
+  // mysql only.  do not rely on this behavior.
+  if (is_string($timezone_from)) $timezone_from = date_db_sql_timezone($timezone_from, TRUE);
+  if (is_string($timezone_to)) $timezone_to = date_db_sql_timezone($timezone_to, TRUE);
+
+  // Set the database timezone to UTC - Views does this too, so it may end up getting set twice,
+  // but it shouldn't matter.
+  static $already_utc = FALSE;
+  if (!$already_utc) {
     switch ($db_type) {
       case('mysql'):
+        if (version_compare(mysql_get_server_info(), '4.1.3', '>=')) {
+          db_query("SET @@session.time_zone = '+00:00'");
+  }
+  break;
       case('mysqli'):
-        $field = " (CONVERT_TZ(NOW(), 'SERVER', $timezone_to))";
+        db_query("SET @@session.time_zone = '+00:00'");
         break;
       case('pgsql'):
-        $field = " (NOW() AT TIMEZONE $timezone_to)";
+        db_query("SET timezone = '+00'");
         break;
     }
+    $already_utc = TRUE;
+  }
+
+  if ($date_type == 'NOW' || $field == 'NOW()') {
+    $field = ' NOW()';
+    if ($timezone_from) {
+      switch ($db_type) {
+        case('mysql'):
+        case('mysqli'):
+          $field = " (CONVERT_TZ($field, '+00:00', $timezone_from[0]))";
+          break;
+        case('pgsql'):
+          $field = " ($field AT TIME ZONE $timezone_from[0])";
+          break;
+      }
+    }
   }
-  // FROM_UNIXTIME() and TIMESTAMP() are timezone adjusted by db
-  // to the server timezone, adjust back to the desired timezone.
   elseif ($date_type == DATE_UNIX && $field) {
+    // $timezone_from is ignored, because UNIX timestamps are always UTC.
     switch ($db_type) {
       case('mysql'):
       case('mysqli'):
-        // Mysql FROM_UNIXTIME always converts from UTC to the server zone,
-        // so if we have a db value stored in some zone other than UTC we
-        // need to do some extra manipulation.
-        if ($timezone_from != 'UTC') {
-          $field = " (CONVERT_TZ(FROM_UNIXTIME($field), 'SERVER', 'UTC'))";
-          $field = " (CONVERT_TZ(FROM_UNIXTIME($field), 'UTC', $timezone_from))";
-          $field = " (CONVERT_TZ(FROM_UNIXTIME($field), $timezone_from, $timezone_to))";
-        }
-        else {
-          $field = " (CONVERT_TZ(FROM_UNIXTIME($field), 'SERVER', $timezone_to))";
+        $field = " FROM_UNIXTIME($field)";
+        if ($timezone_to) {
+          $field = " CONVERT_TZ($field, '+00:00', $timezone_to[0])";
         }
         break;
       case('pgsql'):
-        $field = " ($field::ABSTIME AT TIMEZONE '$timezone_from')";
-        $field = " (SELECT TIMESTAMP WITH TIME ZONE $field $timezone_from AT TIME ZONE $timezone_to)";
+        $field = " TO_TIMESTAMP($field)";
+        if ($timezone_to) {
+          $field = " ($field AT TIME ZONE $timezone_to[0])";
+        }
         break;
     }
   }
-  // ISO dates are not timezone_adjusted by db when converted to dates.
   elseif ($date_type == DATE_ISO && $field) {
     switch ($db_type) {
       case('mysql'):
       case('mysqli'):
-        // Double %% so Drupal won't strip them out.
-        $field = " STR_TO_DATE($field, '%%Y-%%m-%%%%dT%%T')";
-        $field = " (CONVERT_TZ($field, $timezone_from, $timezone_to))";
-        break;
-      case('pgsl'):
-        $field = " TO_DATE($field, 'FMYYYY-FMMM-FMDDTFMHH:FMMI:FMSS')";
-        $field = " (SELECT TIMESTAMP WITH TIME ZONE '$field $timezone_from' AT TIME ZONE $timezone_to)";
+        $field = " CAST($field AS DATETIME)";
+        if ($timezone_from || $timezone_to) {
+          if (!isset($timezone_from)) $timezone_from = array("'+00:00'");
+          else if (!isset($timezone_to)) $timezone_to = $timezone_from;
+          $field = " (CONVERT_TZ($field, $timezone_from[0], $timezone_to[0]))";
+        }
+        break;
+      case('pgsql'):
+        $field = " CAST($field AS TIMESTAMP)";
+        if ($timezone_from || $timezone_to) {
+          if (!isset($timezone_from)) $timezone_from = array("'+00'");
+          else if (!isset($timezone_to)) $timezone_to = $timezone_from;
+          // add the timezone and cast to a timestamp with time zone, and request in $timezone_to.
+          $field = " (CAST($field || $timezone_from[0] AS TIMESTAMP WITH TIME ZONE) AT TIME ZONE $timezone_to[0])";
+        }
         break;
     }
   }
   elseif ($date_type == DATE_DATETIME && $field) {
-    switch ($db_type) {
-      case('mysql'):
-      case('mysqli'):
-        $field = "(CONVERT_TZ('$field', $timezone_from, $timezone_to))";
-        break;
-      case('pgsql'):
-        $field = "(SELECT TIMESTAMP WITH TIME ZONE $field $timezone_from AT TIME ZONE $timezone_to)";
-        break;
+    if ($timezone_from || $timezone_to) {
+      switch ($db_type) {
+        case('mysql'):
+        case('mysqli'):
+          if (!isset($timezone_from)) $timezone_from = array("'+00:00'");
+          else if (!isset($timezone_to)) $timezone_to = $timezone_from;
+
+          $field = " (CONVERT_TZ($field, $timezone_from[0], $timezone_to[0]))";
+          break;
+        case('pgsql'):
+          if (!isset($timezone_from)) $timezone_from = array("'+00'");
+          else if (!isset($timezone_to)) $timezone_to = $timezone_from;
+
+          // convert the field in to a timestamp without time zone so we can add our own
+          $field = " CAST($field AS TIMESTAMP WITHOUT TIME ZONE)";
+          // add the timezone and cast to a timestamp with time zone, and request in $timezone_to.
+          $field = " (CAST($field || $timezone_from[0] AS TIMESTAMP WITH TIME ZONE) AT TIME ZONE $timezone_to[0])";
+          break;
+      }
     }
   }
-  // Now that the date is timezone-adjusted, get the date parts we need.
+
+  // Now that the date is a SQL date/datetime, get the date parts we need.
   return date_sql_extract($result_type, $field);
 }
 
+/**
+ * Returns a time zone or time zone field formatted for use as a database
+ * query.
+ *
+ * @params $timezone - the time zone for time zone field to format
+ * @params $field - whether $timezone is a field or not
+ * @return array(formatted time zone ready for querying, formatted join, unformatted join info)
+ *
+ * example:
+ * date_db_sql_timezone('UTC')
+ *
+ * returns:
+ * array("'UTC'")
+ *
+ * example:
+ * date_db_sql_timezone('MYFIELD', TRUE)
+ *
+ * mysql returns:
+ * array("MYFIELD")
+ *
+ * pgsql returns:
+ * array("_date_pgsql_timezone_0.abbrev", " LEFT JOIN pg_timezone_names _date_pgsql_timezone_0 ON _date_pgsql_timezone_0.name = MYFIELD",
+ *   array('table' => "pg_timezone_names",
+ *         'left_field' => "name",
+ *         'left_field_eq' => "MYFIELD"
+ *         'field' => "abbrev"
+ *   )
+ * );
+ *
+ */
+function date_db_sql_timezone($timezone, $field = FALSE) {
+  global $db_type;
+
+  switch ($db_type) {
+    case('mysql'):
+    case('mysqli'):
+      if (!$field)
+        $timezone = "'$timezone'";
+      return array($timezone);
+      break;
+    case('pgsql');
+      // pgsql requires pgsql time zone abbreviations
+      if ($field) {
+        static $pgsql_timezone_field = array();
+        static $pgsql_field_counter = 0;
+
+        if (!isset($pgsql_timezone_field[$timezone])) {
+          $field = '_date_pgsql_timezone_' . strval($pgsql_field_counter);
+          $join = " LEFT JOIN pg_timezone_names $field ON $field.name = $timezone";
+          $field .= '.abbrev';
+          $pgsql_field_counter++;
+          $pgsql_timezone_field[$timezone] = array($field, $join, array(
+            'table' => 'pg_timezone_names', 'left_field' => 'name', 'left_field_eq' => $timezone, 'field' => 'abbrev'
+          ));
+        }
+
+        return $pgsql_timezone_field[$timezone];
+      } else {
+        // TODO: Which is more efficient?  Queries like this, or stuffing it in a join as above?
+        static $pgsql_timezone = array();
+
+        if (!isset($pgsql_timezone[$timezone_name])) {
+          // Get the full name
+          if (is_string($timezone)) {
+            $timezone_obj = timezone_open($timezone);
+          } else {
+            $timezone_obj = $timezone;
+          }
+          if (!$timezone_obj) return NULL;
+
+          $timezone_name = timezone_name_get($timezone_obj);
+
+          // query postgresql for the abbreviation
+          $timezone_abbr = db_result(db_query("SELECT abbrev FROM pg_timezone_names WHERE name = '$timezone_name'"));
+
+          if (is_string($timezone))
+            $pgsql_timezone[$timezone] = array("'$timezone_abbr'");
+          $pgsql_timezone[$timezone_name] = array("'$timezone_abbr'");
+        }
+
+        return $pgsql_timezone[$timezone_name];
+      }
+      break;
+  }
+
+  return NULL;
+}
+
+/**
+ * Returns a time zone or time zone field formatted for use as a database
+ * query and adds the join to a views query.
+ *
+ * @params $query - the views query to add the join to
+ * @params $timezone - the time zone for time zone field to format
+ * @params $field - whether $timezone is a field or not
+ * @return array(formatted time zone ready for querying, formatted join, unformatted join info)
+ */
+function date_db_sql_timezone_views(&$query, $timezone, $field = FALSE) {
+  $tz_formatted = date_db_sql_timezone($timezone, $field);
+  if (isset($tz_formatted[2])) {
+    // push the join in to something views can understand
+    $joininfo = array(
+      'type' => 'left',
+      'left' => array(
+        'table' => $tz_formatted[2]['table'],
+        'field' => $tz_formatted[2]['left_field']
+      ),
+      'right' => array(
+        'field' => $tz_formatted[2]['left_field_eq']
+      )
+    );
+    $table_num = $query->add_table($tz_formatted[0], false, 1, $joininfo);
+    $tz_formatted[0] = $query->get_table_name($tz_formatted[0], $table_num) .'.'. $tz_formatted[2]['field'];
+  }
+  return $tz_formatted;
+}
 
 /**
  *  Cross-database method of extracting date information from datetime fields.
