On Mon, Jan 30, 2023 at 4:07 PM Tom Lane <[email protected]> wrote:
> Gurjeet Singh <[email protected]> writes:
> > [ generate_series_with_timezone.v6.patch ]
>
> The cfbot isn't terribly happy with this. It looks like UBSan
> is detecting some undefined behavior. Possibly an uninitialized
> variable?
It was the classical case of out-of-bounds access. I was trying to
access 4th argument, even in the case where the 3-argument variant of
generate_series() was called.
Please see attached v7 of the patch. It now checks PG_NARGS() before
accessing the optional parameter.
This mistake would've been caught early if there were assertions
preventing access beyond the number of arguments passed to the
function. I'll send the assert_enough_args.patch, that adds these
checks, in a separate thread to avoid potentially confusing cfbot.
Best regards,
Gurjeet
http://Gurje.et
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index e09e289a43..aa15407936 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -9231,6 +9231,22 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <function>date_add</function> ( <type>timestamp with time zone</type>, <type>interval</type> <optional>, <type>text</type> </optional> )
+ <returnvalue>timestamp with time zone</returnvalue>
+ </para>
+ <para>
+ Add <type>interval</type> to a <type>timestamp with time zone</type> value,
+ at the time zone specified by the third parameter. The time zone value
+ defaults to current <xref linkend="guc-timezone"/> setting.
+ </para>
+ <para>
+ <literal>date_add('2021-10-31 00:00:00+02'::timestamptz, '1 day'::interval, 'Europe/Warsaw')</literal>
+ <returnvalue>2021-10-31 23:00:00</returnvalue>
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<function>date_bin</function> ( <type>interval</type>, <type>timestamp</type>, <type>timestamp</type> )
@@ -9278,6 +9294,22 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <function>date_subtract</function> ( <type>timestamp with time zone</type>, <type>interval</type> <optional>, <type>text</type> </optional> )
+ <returnvalue>timestamp with time zone</returnvalue>
+ </para>
+ <para>
+ Subtract <type>interval</type> from a <type>timestamp with time zone</type> value,
+ at the time zone specified by the third parameter. The time zone value
+ defaults to the current <xref linkend="guc-timezone"/> setting.
+ </para>
+ <para>
+ <literal>date_subtract('2021-10-31 00:00:00+02'::timestamptz, '1 day'::interval, 'Europe/Warsaw')</literal>
+ <returnvalue>2021-10-29 22:00:00</returnvalue>
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
@@ -21968,13 +22000,14 @@ AND
<returnvalue>setof timestamp</returnvalue>
</para>
<para role="func_signature">
- <function>generate_series</function> ( <parameter>start</parameter> <type>timestamp with time zone</type>, <parameter>stop</parameter> <type>timestamp with time zone</type>, <parameter>step</parameter> <type>interval</type> )
+ <function>generate_series</function> ( <parameter>start</parameter> <type>timestamp with time zone</type>, <parameter>stop</parameter> <type>timestamp with time zone</type>, <parameter>step</parameter> <type>interval</type> <optional>, <parameter>timezone</parameter> <type>text</type> </optional> )
<returnvalue>setof timestamp with time zone</returnvalue>
</para>
<para>
Generates a series of values from <parameter>start</parameter>
to <parameter>stop</parameter>, with a step size
- of <parameter>step</parameter>.
+ of <parameter>step</parameter>. <parameter>timezone</parameter>
+ defaults to the current <xref linkend="guc-timezone"/> setting.
</para></entry>
</row>
</tbody>
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 47e059a409..bd85f6421e 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -69,6 +69,7 @@ typedef struct
TimestampTz finish;
Interval step;
int step_sign;
+ pg_tz *attimezone;
} generate_series_timestamptz_fctx;
@@ -78,6 +79,8 @@ static bool AdjustIntervalForTypmod(Interval *interval, int32 typmod,
Node *escontext);
static TimestampTz timestamp2timestamptz(Timestamp timestamp);
static Timestamp timestamptz2timestamp(TimestampTz timestamp);
+static pg_tz* lookup_timezone(text *zone);
+static Datum generate_series_timestamptz_internal(FunctionCallInfo fcinfo);
/* common code for timestamptypmodin and timestamptztypmodin */
@@ -550,6 +553,54 @@ parse_sane_timezone(struct pg_tm *tm, text *zone)
return tz;
}
+/*
+ * Look up the requested timezone (see notes in timestamptz_zone()).
+ */
+static pg_tz *
+lookup_timezone(text *zone)
+{
+ char tzname[TZ_STRLEN_MAX + 1];
+ char *lowzone;
+ int type,
+ dterr,
+ val;
+ pg_tz *tzp;
+
+ DateTimeErrorExtra extra;
+
+ text_to_cstring_buffer(zone, tzname, sizeof(tzname));
+
+ /* DecodeTimezoneAbbrev requires lowercase input */
+ lowzone = downcase_truncate_identifier(tzname,
+ strlen(tzname),
+ false);
+
+ dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
+ if (dterr)
+ DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
+
+ 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)));
+ }
+
+ return tzp;
+}
+
/*
* make_timestamp_internal
* workhorse for make_timestamp and make_timestamptz
@@ -3014,97 +3065,112 @@ timestamp_mi_interval(PG_FUNCTION_ARGS)
}
+/*
+ * timestamptz_pl_interval_internal()
+ * Add an interval to timestamptz, in the given (or session) timezone
+ *
+ * Note that interval has provisions for qualitative year/month and day
+ * units, so try to do the right thing with them.
+ * To add a month, increment the month, and use the same day of month.
+ * Then, if the next month has fewer days, set the day of month
+ * to the last day of month.
+ * To add a day, increment the mday, and use the same time of day.
+ * Lastly, add in the "quantitative time".
+ */
+static TimestampTz
+timestamptz_pl_interval_internal(TimestampTz timestamp,
+ Interval *span,
+ pg_tz *attimezone)
+{
+ int tz;
+
+ if (TIMESTAMP_NOT_FINITE(timestamp))
+ return timestamp;
+
+ /* Use session timezone if caller asks for default */
+ if (attimezone == NULL)
+ attimezone = session_timezone;
+
+ if (span->month != 0)
+ {
+ struct pg_tm tt,
+ *tm = &tt;
+ fsec_t fsec;
+
+ if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, attimezone) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ tm->tm_mon += span->month;
+ if (tm->tm_mon > MONTHS_PER_YEAR)
+ {
+ tm->tm_year += (tm->tm_mon - 1) / MONTHS_PER_YEAR;
+ tm->tm_mon = ((tm->tm_mon - 1) % MONTHS_PER_YEAR) + 1;
+ }
+ else if (tm->tm_mon < 1)
+ {
+ tm->tm_year += tm->tm_mon / MONTHS_PER_YEAR - 1;
+ tm->tm_mon = tm->tm_mon % MONTHS_PER_YEAR + MONTHS_PER_YEAR;
+ }
+
+ /* adjust for end of month boundary problems... */
+ if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1])
+ tm->tm_mday = (day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]);
+
+ tz = DetermineTimeZoneOffset(tm, attimezone);
+
+ if (tm2timestamp(tm, fsec, &tz, ×tamp) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+ }
+
+ if (span->day != 0)
+ {
+ struct pg_tm tt,
+ *tm = &tt;
+ fsec_t fsec;
+ int julian;
+
+ if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, attimezone) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ /* Add days by converting to and from Julian */
+ julian = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) + span->day;
+ j2date(julian, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
+
+ tz = DetermineTimeZoneOffset(tm, attimezone);
+
+ if (tm2timestamp(tm, fsec, &tz, ×tamp) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+ }
+
+ timestamp += span->time;
+
+ if (!IS_VALID_TIMESTAMP(timestamp))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+ errmsg("timestamp out of range")));
+
+ return timestamp;
+}
+
+
/* timestamptz_pl_interval()
- * Add an interval to a timestamp with time zone data type.
- * Note that interval has provisions for qualitative year/month
- * units, so try to do the right thing with them.
- * To add a month, increment the month, and use the same day of month.
- * Then, if the next month has fewer days, set the day of month
- * to the last day of month.
- * Lastly, add in the "quantitative time".
+ * Add an interval to a timestamptz, in session timezone.
*/
Datum
timestamptz_pl_interval(PG_FUNCTION_ARGS)
{
TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(0);
Interval *span = PG_GETARG_INTERVAL_P(1);
- TimestampTz result;
- int tz;
- if (TIMESTAMP_NOT_FINITE(timestamp))
- result = timestamp;
- else
- {
- if (span->month != 0)
- {
- struct pg_tm tt,
- *tm = &tt;
- fsec_t fsec;
-
- if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
- ereport(ERROR,
- (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
- errmsg("timestamp out of range")));
-
- tm->tm_mon += span->month;
- if (tm->tm_mon > MONTHS_PER_YEAR)
- {
- tm->tm_year += (tm->tm_mon - 1) / MONTHS_PER_YEAR;
- tm->tm_mon = ((tm->tm_mon - 1) % MONTHS_PER_YEAR) + 1;
- }
- else if (tm->tm_mon < 1)
- {
- tm->tm_year += tm->tm_mon / MONTHS_PER_YEAR - 1;
- tm->tm_mon = tm->tm_mon % MONTHS_PER_YEAR + MONTHS_PER_YEAR;
- }
-
- /* adjust for end of month boundary problems... */
- if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1])
- tm->tm_mday = (day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]);
-
- tz = DetermineTimeZoneOffset(tm, session_timezone);
-
- if (tm2timestamp(tm, fsec, &tz, ×tamp) != 0)
- ereport(ERROR,
- (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
- errmsg("timestamp out of range")));
- }
-
- if (span->day != 0)
- {
- struct pg_tm tt,
- *tm = &tt;
- fsec_t fsec;
- int julian;
-
- if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
- ereport(ERROR,
- (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
- errmsg("timestamp out of range")));
-
- /* Add days by converting to and from Julian */
- julian = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) + span->day;
- j2date(julian, &tm->tm_year, &tm->tm_mon, &tm->tm_mday);
-
- tz = DetermineTimeZoneOffset(tm, session_timezone);
-
- if (tm2timestamp(tm, fsec, &tz, ×tamp) != 0)
- ereport(ERROR,
- (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
- errmsg("timestamp out of range")));
- }
-
- timestamp += span->time;
-
- if (!IS_VALID_TIMESTAMP(timestamp))
- ereport(ERROR,
- (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
- errmsg("timestamp out of range")));
-
- result = timestamp;
- }
-
- PG_RETURN_TIMESTAMP(result);
+ PG_RETURN_TIMESTAMP(timestamptz_pl_interval_internal(timestamp, span, NULL));
}
Datum
@@ -3118,11 +3184,38 @@ timestamptz_mi_interval(PG_FUNCTION_ARGS)
tspan.day = -span->day;
tspan.time = -span->time;
- return DirectFunctionCall2(timestamptz_pl_interval,
- TimestampGetDatum(timestamp),
- PointerGetDatum(&tspan));
+ PG_RETURN_TIMESTAMP(timestamptz_pl_interval_internal(timestamp, &tspan, NULL));
}
+/* timestamptz_pl_interval_at_zone()
+ * Add an interval to a timestamp with time zone data type in specified timezone.
+ */
+Datum
+timestamptz_pl_interval_at_zone(PG_FUNCTION_ARGS)
+{
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(0);
+ Interval *span = PG_GETARG_INTERVAL_P(1);
+ text *zone = PG_GETARG_TEXT_PP(2);
+ pg_tz *attimezone = lookup_timezone(zone);
+
+ PG_RETURN_TIMESTAMP(timestamptz_pl_interval_internal(timestamp, span, attimezone));
+}
+
+Datum
+timestamptz_mi_interval_at_zone(PG_FUNCTION_ARGS)
+{
+ TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(0);
+ Interval *span = PG_GETARG_INTERVAL_P(1);
+ text *zone = PG_GETARG_TEXT_PP(2);
+ pg_tz *attimezone = lookup_timezone(zone);
+ Interval tspan;
+
+ tspan.month = -span->month;
+ tspan.day = -span->day;
+ tspan.time = -span->time;
+
+ PG_RETURN_TIMESTAMP(timestamptz_pl_interval_internal(timestamp, &tspan, attimezone));
+}
Datum
interval_um(PG_FUNCTION_ARGS)
@@ -4300,13 +4393,7 @@ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(1);
text *zone = PG_GETARG_TEXT_PP(2);
TimestampTz result;
- char tzname[TZ_STRLEN_MAX + 1];
- char *lowzone;
- int dterr,
- type,
- val;
pg_tz *tzp;
- DateTimeErrorExtra extra;
/*
* timestamptz_zone() doesn't look up the zone for infinite inputs, so we
@@ -4315,38 +4402,7 @@ timestamptz_trunc_zone(PG_FUNCTION_ARGS)
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);
-
- dterr = DecodeTimezoneAbbrev(0, lowzone, &type, &val, &tzp, &extra);
- if (dterr)
- DateTimeParseError(dterr, &extra, NULL, NULL, NULL);
-
- 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)));
- }
+ tzp = lookup_timezone(zone);
result = timestamptz_trunc_internal(units, timestamp, tzp);
@@ -5675,6 +5731,9 @@ timestamptz2timestamp(TimestampTz timestamp)
/* timestamptz_zone()
* Evaluate timestamp with time zone type at the specified time zone.
* Returns a timestamp without time zone.
+ *
+ * Note: If you change anything here, also review the code in
+ * lookup_timezone().
*/
Datum
timestamptz_zone(PG_FUNCTION_ARGS)
@@ -5881,6 +5940,22 @@ generate_series_timestamp(PG_FUNCTION_ARGS)
*/
Datum
generate_series_timestamptz(PG_FUNCTION_ARGS)
+{
+ return generate_series_timestamptz_internal(fcinfo);
+}
+
+/* generate_series_timestamptz_at_zone()
+ * Generate the set of timestamps from start to finish by step, in the
+ * specified timezone.
+ */
+Datum
+generate_series_timestamptz_at_zone(PG_FUNCTION_ARGS)
+{
+ return generate_series_timestamptz_internal(fcinfo);
+}
+
+static Datum
+generate_series_timestamptz_internal(FunctionCallInfo fcinfo)
{
FuncCallContext *funcctx;
generate_series_timestamptz_fctx *fctx;
@@ -5892,8 +5967,11 @@ generate_series_timestamptz(PG_FUNCTION_ARGS)
TimestampTz start = PG_GETARG_TIMESTAMPTZ(0);
TimestampTz finish = PG_GETARG_TIMESTAMPTZ(1);
Interval *step = PG_GETARG_INTERVAL_P(2);
- MemoryContext oldcontext;
- const Interval interval_zero = {0};
+ text *zone = (PG_NARGS() == 4 && !PG_ARGISNULL(3))
+ ? PG_GETARG_TEXT_PP(3) : NULL;
+
+ MemoryContext oldcontext;
+ const Interval interval_zero = {0};
/* create a function context for cross-call persistence */
funcctx = SRF_FIRSTCALL_INIT();
@@ -5914,6 +5992,7 @@ generate_series_timestamptz(PG_FUNCTION_ARGS)
fctx->current = start;
fctx->finish = finish;
fctx->step = *step;
+ fctx->attimezone = zone ? lookup_timezone(zone) : NULL;
/* Determine sign of the interval */
fctx->step_sign = interval_cmp_internal(&fctx->step, &interval_zero);
@@ -5941,9 +6020,7 @@ generate_series_timestamptz(PG_FUNCTION_ARGS)
timestamp_cmp_internal(result, fctx->finish) >= 0)
{
/* increment current in preparation for next iteration */
- fctx->current = DatumGetTimestampTz(DirectFunctionCall2(timestamptz_pl_interval,
- TimestampTzGetDatum(fctx->current),
- PointerGetDatum(&fctx->step)));
+ fctx->current = timestamptz_pl_interval_internal(fctx->current, &fctx->step, fctx->attimezone);
/* do when there is more left to send */
SRF_RETURN_NEXT(funcctx, TimestampTzGetDatum(result));
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c0f2a8a77c..79300f1317 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2426,6 +2426,26 @@
proname => 'timestamptz_pl_interval', provolatile => 's',
prorettype => 'timestamptz', proargtypes => 'timestamptz interval',
prosrc => 'timestamptz_pl_interval' },
+{ oid => '8800',
+ descr => 'add interval to timestamp with time zone',
+ proname => 'date_add', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'timestamptz interval',
+ prosrc => 'timestamptz_pl_interval' },
+{ oid => '8801',
+ descr => 'add interval to timestamp with time zone in specified time zone',
+ proname => 'date_add',
+ prorettype => 'timestamptz', proargtypes => 'timestamptz interval text',
+ prosrc => 'timestamptz_pl_interval_at_zone' },
+{ oid => '8802',
+ descr => 'subtract interval from timestamp with time zone',
+ proname => 'date_subtract', provolatile => 's',
+ prorettype => 'timestamptz', proargtypes => 'timestamptz interval',
+ prosrc => 'timestamptz_mi_interval' },
+{ oid => '8803',
+ descr => 'subtract interval from timestamp with time zone in specified time zone',
+ proname => 'date_subtract',
+ prorettype => 'timestamptz', proargtypes => 'timestamptz interval text',
+ prosrc => 'timestamptz_mi_interval_at_zone' },
{ oid => '1190',
proname => 'timestamptz_mi_interval', provolatile => 's',
prorettype => 'timestamptz', proargtypes => 'timestamptz interval',
@@ -8232,6 +8252,11 @@
provolatile => 's', prorettype => 'timestamptz',
proargtypes => 'timestamptz timestamptz interval',
prosrc => 'generate_series_timestamptz' },
+{ oid => '8804', descr => 'non-persistent series generator',
+ proname => 'generate_series', prorows => '1000', proretset => 't',
+ prorettype => 'timestamptz',
+ proargtypes => 'timestamptz timestamptz interval text',
+ prosrc => 'generate_series_timestamptz_at_zone' },
# boolean aggregates
{ oid => '2515', descr => 'aggregate transition function',
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index b120f5e7fe..a445ac56b9 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -206,7 +206,7 @@ extern void fmgr_symbol(Oid functionId, char **mod, char **fn);
* If function is not marked "proisstrict" in pg_proc, it must check for
* null arguments using this macro. Do not try to GETARG a null argument!
*/
-#define PG_ARGISNULL(n) (fcinfo->args[n].isnull)
+#define PG_ARGISNULL(n) (AssertMacro(n < PG_NARGS()), fcinfo->args[n].isnull)
/*
* Support for fetching detoasted copies of toastable datatypes (all of
@@ -265,7 +265,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum);
/* Macros for fetching arguments of standard types */
-#define PG_GETARG_DATUM(n) (fcinfo->args[n].value)
+#define PG_GETARG_DATUM(n) (AssertMacro(n < PG_NARGS()), fcinfo->args[n].value)
#define PG_GETARG_INT32(n) DatumGetInt32(PG_GETARG_DATUM(n))
#define PG_GETARG_UINT32(n) DatumGetUInt32(PG_GETARG_DATUM(n))
#define PG_GETARG_INT16(n) DatumGetInt16(PG_GETARG_DATUM(n))
diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out
index 00379fd0fd..3d2d479723 100644
--- a/src/test/regress/expected/timestamptz.out
+++ b/src/test/regress/expected/timestamptz.out
@@ -2459,6 +2459,60 @@ select * from generate_series('2020-01-01 00:00'::timestamptz,
'2020-01-02 03:00'::timestamptz,
'0 hour'::interval);
ERROR: step size cannot equal zero
+-- Interval crossing time shift for Europe/Warsaw timezone (with DST)
+SET TimeZone to 'UTC';
+SELECT date_add('2022-10-30 00:00:00+01'::timestamptz,
+ '1 day'::interval);
+ date_add
+------------------------------
+ Sun Oct 30 23:00:00 2022 UTC
+(1 row)
+
+SELECT date_add('2021-10-31 00:00:00+02'::timestamptz,
+ '1 day'::interval,
+ 'Europe/Warsaw');
+ date_add
+------------------------------
+ Sun Oct 31 23:00:00 2021 UTC
+(1 row)
+
+SELECT date_subtract('2022-10-30 00:00:00+01'::timestamptz,
+ '1 day'::interval);
+ date_subtract
+------------------------------
+ Fri Oct 28 23:00:00 2022 UTC
+(1 row)
+
+SELECT date_subtract('2021-10-31 00:00:00+02'::timestamptz,
+ '1 day'::interval,
+ 'Europe/Warsaw');
+ date_subtract
+------------------------------
+ Fri Oct 29 22:00:00 2021 UTC
+(1 row)
+
+SELECT * FROM generate_series('2020-12-31 23:00:00+00'::timestamptz,
+ '2021-12-31 23:00:00+00'::timestamptz,
+ '1 month'::interval,
+ 'Europe/Warsaw');
+ generate_series
+------------------------------
+ Thu Dec 31 23:00:00 2020 UTC
+ Sun Jan 31 23:00:00 2021 UTC
+ Sun Feb 28 23:00:00 2021 UTC
+ Wed Mar 31 22:00:00 2021 UTC
+ Fri Apr 30 22:00:00 2021 UTC
+ Mon May 31 22:00:00 2021 UTC
+ Wed Jun 30 22:00:00 2021 UTC
+ Sat Jul 31 22:00:00 2021 UTC
+ Tue Aug 31 22:00:00 2021 UTC
+ Thu Sep 30 22:00:00 2021 UTC
+ Sun Oct 31 23:00:00 2021 UTC
+ Tue Nov 30 23:00:00 2021 UTC
+ Fri Dec 31 23:00:00 2021 UTC
+(13 rows)
+
+RESET TimeZone;
--
-- Test behavior with a dynamic (time-varying) timezone abbreviation.
-- These tests rely on the knowledge that MSK (Europe/Moscow standard time)
diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql
index 4905dd0831..1a98aa64c3 100644
--- a/src/test/regress/sql/timestamptz.sql
+++ b/src/test/regress/sql/timestamptz.sql
@@ -455,6 +455,25 @@ select * from generate_series('2020-01-01 00:00'::timestamptz,
'2020-01-02 03:00'::timestamptz,
'0 hour'::interval);
+-- Interval crossing time shift for Europe/Warsaw timezone (with DST)
+SET TimeZone to 'UTC';
+
+SELECT date_add('2022-10-30 00:00:00+01'::timestamptz,
+ '1 day'::interval);
+SELECT date_add('2021-10-31 00:00:00+02'::timestamptz,
+ '1 day'::interval,
+ 'Europe/Warsaw');
+SELECT date_subtract('2022-10-30 00:00:00+01'::timestamptz,
+ '1 day'::interval);
+SELECT date_subtract('2021-10-31 00:00:00+02'::timestamptz,
+ '1 day'::interval,
+ 'Europe/Warsaw');
+SELECT * FROM generate_series('2020-12-31 23:00:00+00'::timestamptz,
+ '2021-12-31 23:00:00+00'::timestamptz,
+ '1 month'::interval,
+ 'Europe/Warsaw');
+RESET TimeZone;
+
--
-- Test behavior with a dynamic (time-varying) timezone abbreviation.
-- These tests rely on the knowledge that MSK (Europe/Moscow standard time)