I wrote:
> BTW, I'd been hoping that we could avoid rotate-to-local-and-back
> in Vik's desired case, but after further thought I suspect the only
> real optimization that's possible compared to writing it out with
> two AT TIME ZONE constructs is to do the zone name lookup just once.
> As an example, truncating to a day-or-larger boundary could result in
> shifting to a different UTC offset than you started with, due to crossing
> a DST boundary.

Here's a v2 that transposes the code to C so that we can get that
optimization.  I've not tried to time it, but it should actually be
a bit faster than standard date_trunc plus one AT TIME ZONE rotation,
never mind two of them.

                        regards, tom lane

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1678c8c..adffa7d 100644
*** a/doc/src/sgml/func.sgml
--- b/doc/src/sgml/func.sgml
*************** SELECT regexp_match('abc01234xyz', '(?:(
*** 7186,7191 ****
--- 7186,7200 ----
         </row>
  
         <row>
+         <entry><literal><function>date_trunc(<type>text</type>, <type>timestamp with time zone</type>, <type>text</type>)</function></literal></entry>
+         <entry><type>timestamp with time zone</type></entry>
+         <entry>Truncate to specified precision in the specified time zone; see also <xref linkend="functions-datetime-trunc"/>
+         </entry>
+         <entry><literal>date_trunc('day', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney')</literal></entry>
+         <entry><literal>2001-02-16 13:00:00+00</literal></entry>
+        </row>
+ 
+        <row>
          <entry><literal><function>date_trunc(<type>text</type>, <type>interval</type>)</function></literal></entry>
          <entry><type>interval</type></entry>
          <entry>Truncate to specified precision; see also <xref linkend="functions-datetime-trunc"/>
*************** SELECT date_part('hour', INTERVAL '4 hou
*** 8078,8084 ****
  
     <para>
  <synopsis>
! date_trunc('<replaceable>field</replaceable>', <replaceable>source</replaceable>)
  </synopsis>
      <replaceable>source</replaceable> is a value expression of type
      <type>timestamp</type> or <type>interval</type>.
--- 8087,8093 ----
  
     <para>
  <synopsis>
! date_trunc(<replaceable>field</replaceable>, <replaceable>source</replaceable> [, <replaceable>time_zone</replaceable>])
  </synopsis>
      <replaceable>source</replaceable> is a value expression of type
      <type>timestamp</type> or <type>interval</type>.
*************** date_trunc('<replaceable>field</replacea
*** 8112,8124 ****
     </para>
  
     <para>
!     Examples:
  <screen>
  SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 20:00:00</computeroutput>
  
  SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-01-01 00:00:00</computeroutput>
  </screen>
     </para>
    </sect2>
--- 8121,8158 ----
     </para>
  
     <para>
!     If the optional <replaceable>time_zone</replaceable> argument is
!     present, the <replaceable>source</replaceable> value is truncated in the
!     specified time zone; for example, truncation to <literal>day</literal>
!     produces a value that is midnight in that zone.  The time zone name can
!     be specified in any of the ways described in
!     <xref linkend="datatype-timezones"/>.
!    </para>
! 
!    <para>
!     When the <replaceable>time_zone</replaceable> argument is
!     present, the <replaceable>source</replaceable> and result are always of
!     type <type>timestamp with time zone</type>, whereas the two-argument
!     form of <function>date_trunc</function> is available for timestamps with
!     or without time zone.  The two-argument form truncates <type>timestamp
!     with time zone</type> values using the current
!     <xref linkend="guc-timezone"/> setting.
!    </para>
! 
!    <para>
!     Examples (assuming the local time zone is <literal>America/New_York</literal>):
  <screen>
  SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 20:00:00</computeroutput>
  
  SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40');
  <lineannotation>Result: </lineannotation><computeroutput>2001-01-01 00:00:00</computeroutput>
+ 
+ SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00');
+ <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 00:00:00-05</computeroutput>
+ 
+ SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', 'Australia/Sydney');
+ <lineannotation>Result: </lineannotation><computeroutput>2001-02-16 08:00:00-05</computeroutput>
  </screen>
     </para>
    </sect2>
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 449164a..e5f8b51 100644
*** a/src/backend/utils/adt/timestamp.c
--- b/src/backend/utils/adt/timestamp.c
*************** timestamp_trunc(PG_FUNCTION_ARGS)
*** 3925,3938 ****
  	PG_RETURN_TIMESTAMP(result);
  }
  
! /* timestamptz_trunc()
!  * Truncate timestamp to specified units.
   */
! Datum
! timestamptz_trunc(PG_FUNCTION_ARGS)
  {
- 	text	   *units = PG_GETARG_TEXT_PP(0);
- 	TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
  	TimestampTz result;
  	int			tz;
  	int			type,
--- 3925,3939 ----
  	PG_RETURN_TIMESTAMP(result);
  }
  
! /*
!  * Common code for timestamptz_trunc() and timestamptz_trunc_zone().
!  *
!  * tzp identifies the zone to truncate with respect to.  We assume
!  * infinite timestamps have already been rejected.
   */
! static TimestampTz
! timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp)
  {
  	TimestampTz result;
  	int			tz;
  	int			type,
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 3943,3951 ****
  	struct pg_tm tt,
  			   *tm = &tt;
  
- 	if (TIMESTAMP_NOT_FINITE(timestamp))
- 		PG_RETURN_TIMESTAMPTZ(timestamp);
- 
  	lowunits = downcase_truncate_identifier(VARDATA_ANY(units),
  											VARSIZE_ANY_EXHDR(units),
  											false);
--- 3944,3949 ----
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 3954,3960 ****
  
  	if (type == UNITS)
  	{
! 		if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
  			ereport(ERROR,
  					(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
  					 errmsg("timestamp out of range")));
--- 3952,3958 ----
  
  	if (type == UNITS)
  	{
! 		if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, tzp) != 0)
  			ereport(ERROR,
  					(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
  					 errmsg("timestamp out of range")));
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 4055,4061 ****
  		}
  
  		if (redotz)
! 			tz = DetermineTimeZoneOffset(tm, session_timezone);
  
  		if (tm2timestamp(tm, fsec, &tz, &result) != 0)
  			ereport(ERROR,
--- 4053,4059 ----
  		}
  
  		if (redotz)
! 			tz = DetermineTimeZoneOffset(tm, tzp);
  
  		if (tm2timestamp(tm, fsec, &tz, &result) != 0)
  			ereport(ERROR,
*************** timestamptz_trunc(PG_FUNCTION_ARGS)
*** 4071,4076 ****
--- 4069,4151 ----
  		result = 0;
  	}
  
+ 	return result;
+ }
+ 
+ /* timestamptz_trunc()
+  * Truncate timestamptz to specified units.
+  */
+ Datum
+ timestamptz_trunc(PG_FUNCTION_ARGS)
+ {
+ 	text	   *units = PG_GETARG_TEXT_PP(0);
+ 	TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ 	TimestampTz result;
+ 
+ 	if (TIMESTAMP_NOT_FINITE(timestamp))
+ 		PG_RETURN_TIMESTAMPTZ(timestamp);
+ 
+ 	result = timestamptz_trunc_internal(units, timestamp, session_timezone);
+ 
+ 	PG_RETURN_TIMESTAMPTZ(result);
+ }
+ 
+ /* timestamptz_trunc_zone()
+  * Truncate timestamptz to specified units in specified timezone.
+  */
+ Datum
+ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
+ {
+ 	text	   *units = PG_GETARG_TEXT_PP(0);
+ 	TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
+ 	text	   *zone = PG_GETARG_TEXT_PP(2);
+ 	TimestampTz result;
+ 	char		tzname[TZ_STRLEN_MAX + 1];
+ 	char	   *lowzone;
+ 	int			type,
+ 				val;
+ 	pg_tz	   *tzp;
+ 
+ 	/*
+ 	 * timestamptz_zone() doesn't look up the zone for infinite inputs, so we
+ 	 * don't do so here either.
+ 	 */
+ 	if (TIMESTAMP_NOT_FINITE(timestamp))
+ 		PG_RETURN_TIMESTAMP(timestamp);
+ 
+ 	/*
+ 	 * Look up the requested timezone (see notes in timestamptz_zone()).
+ 	 */
+ 	text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+ 
+ 	/* DecodeTimezoneAbbrev requires lowercase input */
+ 	lowzone = downcase_truncate_identifier(tzname,
+ 										   strlen(tzname),
+ 										   false);
+ 
+ 	type = DecodeTimezoneAbbrev(0, lowzone, &val, &tzp);
+ 
+ 	if (type == TZ || type == DTZ)
+ 	{
+ 		/* fixed-offset abbreviation, get a pg_tz descriptor for that */
+ 		tzp = pg_tzset_offset(-val);
+ 	}
+ 	else if (type == DYNTZ)
+ 	{
+ 		/* dynamic-offset abbreviation, use its referenced timezone */
+ 	}
+ 	else
+ 	{
+ 		/* try it as a full zone name */
+ 		tzp = pg_tzset(tzname);
+ 		if (!tzp)
+ 			ereport(ERROR,
+ 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ 					 errmsg("time zone \"%s\" not recognized", tzname)));
+ 	}
+ 
+ 	result = timestamptz_trunc_internal(units, timestamp, tzp);
+ 
  	PG_RETURN_TIMESTAMPTZ(result);
  }
  
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4026018..9264a2e 100644
*** a/src/include/catalog/pg_proc.dat
--- b/src/include/catalog/pg_proc.dat
***************
*** 2280,2285 ****
--- 2280,2289 ----
    descr => 'truncate timestamp with time zone to specified units',
    proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
    proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' },
+ { oid => '1284',
+   descr => 'truncate timestamp with time zone to specified units in specified time zone',
+   proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
+   proargtypes => 'text timestamptz text', prosrc => 'timestamptz_trunc_zone' },
  { oid => '1218', descr => 'truncate interval to specified units',
    proname => 'date_trunc', prorettype => 'interval',
    proargtypes => 'text interval', prosrc => 'interval_trunc' },
diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out
index 2340f30..8a4c719 100644
*** a/src/test/regress/expected/timestamptz.out
--- b/src/test/regress/expected/timestamptz.out
*************** SELECT '' AS date_trunc_week, date_trunc
*** 649,654 ****
--- 649,672 ----
                   | Mon Feb 23 00:00:00 2004 PST
  (1 row)
  
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'Australia/Sydney') as sydney_trunc;  -- zone name
+  date_trunc_at_tz |         sydney_trunc         
+ ------------------+------------------------------
+                   | Fri Feb 16 05:00:00 2001 PST
+ (1 row)
+ 
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as gmt_trunc;  -- fixed-offset abbreviation
+  date_trunc_at_tz |          gmt_trunc           
+ ------------------+------------------------------
+                   | Thu Feb 15 16:00:00 2001 PST
+ (1 row)
+ 
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as vet_trunc;  -- variable-offset abbreviation
+  date_trunc_at_tz |          vet_trunc           
+ ------------------+------------------------------
+                   | Thu Feb 15 20:00:00 2001 PST
+ (1 row)
+ 
  -- Test casting within a BETWEEN qualifier
  SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
    FROM TIMESTAMPTZ_TBL
diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql
index f17d153..c3bd46c 100644
*** a/src/test/regress/sql/timestamptz.sql
--- b/src/test/regress/sql/timestamptz.sql
*************** SELECT '' AS "54", d1 - timestamp with t
*** 193,198 ****
--- 193,202 ----
  
  SELECT '' AS date_trunc_week, date_trunc( 'week', timestamp with time zone '2004-02-29 15:44:17.71393' ) AS week_trunc;
  
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'Australia/Sydney') as sydney_trunc;  -- zone name
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as gmt_trunc;  -- fixed-offset abbreviation
+ SELECT '' AS date_trunc_at_tz, date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as vet_trunc;  -- variable-offset abbreviation
+ 
  -- Test casting within a BETWEEN qualifier
  SELECT '' AS "54", d1 - timestamp with time zone '1997-01-02' AS diff
    FROM TIMESTAMPTZ_TBL

Reply via email to