From 5ba2b6745e95bb68674444ceba4fcb07102249a7 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Fri, 21 Aug 2026 07:14:01 +0100 Subject: [PATCH-v15.3 6/8] SLOPE Timezone Add slope support to functions that use a `timestamp` computed from a `timestamp with timezone`. Time in certain time zones might have discontinuities, mostly due to dailight saving time (DST), but maybe others. The `timestamp with timezone` is represented with resolution of 1 microsecond at UTC, when converted to a `timestamp` the session timezone offset of the timezone at the given time is computed and applied, the offset varies with time, and the two common discontinuities are a spring-forward when the clock advances creating effectively skipping a range of timestamps, or a fall-back when the clock moves backward, creating a range of ambiguous timestamps. The fall-back discontinuities cause the timestamp to be discontinuous, but certain parts of the timestamp might remain monotonic. The most relevant example is the date of a timestamp, in most of the timezones, a fall back stays within the same day. Follows a query to the discontinuities discussed above. WITH RECURSIVE bin AS ( -- search a intervals of duration t_step SELECT n.name AS tz, t AS lo, t + t_step AS hi, t_step FROM pg_timezone_names n, (VALUES (timestamptz '1900-01-01', timestamptz '2026-01-01', interval '7 days')) AS range(t_from, t_to, t_step), generate_series(t_from, t_to, t_step) AS t WHERE -- keep only those having a discontinuity timezone(name, t + t_step) - timezone(name, t) <> t_step UNION ALL SELECT tz, CASE WHEN pick_hi THEN mid ELSE lo END, CASE WHEN pick_hi THEN hi ELSE mid END, t_step / 2 FROM bin, LATERAL (SELECT (lo + t_step / 2) AS mid, timezone(tz, lo + t_step / 2) - timezone(tz, lo) = t_step / 2 as pick_hi ) WHERE t_step >= interval '2 microseconds' ), discontinuities AS ( SELECT tz, hi AS utc, timezone(tz, lo) AS before, timezone(tz, hi) AS after FROM bin WHERE hi - lo < interval '2 microseconds' ) SELECT * FROM discontinuities -- Having a list of discontinuities, it is easier to respond -- questions such as which discontinuities caused the date to -- move back WHERE after::date < before::date The functions supported now are - date_trunc(unit text, timestamptz) - date_trunc(unit text, timestamptz, timezone text) - timezone(timestamptz) - timezone(timezone text, timestamptz) - date(timestamptz) The timezone monotonicity is performed once when the timezone is loaded and checked when the monotonicity of one of those functions is requested. --- src/backend/utils/adt/timestamp.c | 8 +- src/backend/utils/fmgr/slopesupport.c | 197 +++++++++++++++++ src/include/c.h | 5 + src/include/catalog/pg_proc.dat | 22 +- src/include/pgtime.h | 19 ++ src/test/regress/expected/slope.out | 222 +++++++++++++++++--- src/test/regress/expected/slope_catalog.out | 156 +++++++------- src/test/regress/sql/slope.sql | 123 +++++++++-- src/test/regress/sql/slope_catalog.sql | 13 +- src/timezone/localtime.c | 15 ++ src/timezone/pgtz.c | 161 +++++++++++++- src/timezone/pgtz.h | 7 + 12 files changed, 817 insertions(+), 131 deletions(-) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 9c17ba2f905..18e94d7bb03 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -1921,10 +1921,6 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, const char Timestamp time; pg_time_t utime; - /* Use session timezone if caller asks for default */ - if (attimezone == NULL) - attimezone = session_timezone; - time = dt; TMODULO(time, date, USECS_PER_DAY); @@ -1970,7 +1966,9 @@ timestamp2tm(Timestamp dt, int *tzp, struct pg_tm *tm, fsec_t *fsec, const char utime = (pg_time_t) dt; if ((Timestamp) utime == dt) { - struct pg_tm *tx = pg_localtime(&utime, attimezone); + /* Use session timezone doesn't pass one */ + struct pg_tm *tx = pg_localtime(&utime, + attimezone == NULL ? session_timezone : attimezone); tm->tm_year = tx->tm_year + 1900; tm->tm_mon = tx->tm_mon + 1; diff --git a/src/backend/utils/fmgr/slopesupport.c b/src/backend/utils/fmgr/slopesupport.c index 69ae8d39bf5..7c02de01e7d 100644 --- a/src/backend/utils/fmgr/slopesupport.c +++ b/src/backend/utils/fmgr/slopesupport.c @@ -3,9 +3,17 @@ #include #include "c.h" +#include "catalog/pg_type.h" +#include "nodes/primnodes.h" #include "nodes/supportnodes.h" +#include "parser/scansup.h" +#include "pgtime.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/datetime.h" #include "utils/fmgrprotos.h" #include "utils/numeric.h" +#include "varatt.h" /** * Extended numeric sign, the usual -1, 0, 1, @@ -342,3 +350,192 @@ divide_slope_support(PG_FUNCTION_ARGS) } PG_RETURN_POINTER(NULL); } + +/* + * Look up a timezone from a constant text argument. + */ +static pg_tz * +get_const_timezone_arg(List *args, int argno) +{ + Node *tz_arg_node; + Const *tz_const; + text *zone; + char tzname[TZ_STRLEN_MAX + 1]; + + if (args == NULL || list_length(args) <= argno) + return NULL; + + tz_arg_node = list_nth(args, argno); + if (!IsA(tz_arg_node, Const)) + return NULL; + + tz_const = (Const *) tz_arg_node; + + if (tz_const->constisnull || tz_const->consttype != TEXTOID) + return NULL; + + zone = DatumGetTextPP(tz_const->constvalue); + text_to_cstring_buffer(zone, tzname, sizeof(tzname)); + return DecodeTimezoneNameToTz(tzname); +} + +/* + * Slope support for date(timestamptz) + */ +Datum +timestamptz_date_slope_support(PG_FUNCTION_ARGS) +{ + SLOPE_REQUEST(req); + + if (pg_timezone_is_monotonic(session_timezone, TZ_GAP_DAY, false)) + return monotonic_slope_support(req, 1, asc0_slope); + else + PG_RETURN_POINTER(NULL); +} + +static Oid +get_monotonic_expr_funcid(SupportRequestMonotonic *req) +{ + Node *expr = req->expr; + if (IsA(expr, FuncExpr)) + return ((FuncExpr *) expr)->funcid; + return InvalidOid; +} + +static Datum +timestamptz_support_lookup(SupportRequestMonotonic *req, Datum fixed_unit, pg_tz *tzp) +{ + text *units; + int val; + char *lowunits; + int type; + TZMonotonicityBits tz_unit; + + units = DatumGetTextPP(fixed_unit); + + lowunits = downcase_truncate_identifier(VARDATA_ANY(units), + VARSIZE_ANY_EXHDR(units), + false); + + type = DecodeUnits(0, lowunits, &val); + + if (type != UNITS) + PG_RETURN_POINTER(NULL); + + switch (val) + { + case DTK_WEEK: + PG_RETURN_POINTER(NULL); + case DTK_MILLENNIUM: + case DTK_CENTURY: + case DTK_DECADE: + case DTK_YEAR: + tz_unit = TZ_GAP_YEAR; + break; + case DTK_QUARTER: + case DTK_MONTH: + tz_unit = TZ_GAP_MONTH; + break; + case DTK_DAY: + tz_unit = TZ_GAP_DAY; + break; + case DTK_HOUR: + tz_unit = TZ_GAP_HOUR; + break; + case DTK_MINUTE: + tz_unit = TZ_GAP_MINUTE; + break; + case DTK_SECOND: + case DTK_MILLISEC: + case DTK_MICROSEC: + tz_unit = TZ_GAP_SECOND; + break; + default: + PG_RETURN_POINTER(NULL); + } + if (pg_timezone_is_monotonic(tzp, tz_unit, false)) + return monotonic_slope_support(req, 2, asc1_slope); + else + PG_RETURN_POINTER(NULL); +} + +/* + * Prosupport for timezone(...) overloads with timestamp types. + */ +Datum +timezone_prosupport(PG_FUNCTION_ARGS) +{ + pg_tz *tzp; + bool to_utc = false; + SLOPE_REQUEST_ARGS(req, args, 1); + + + switch (get_monotonic_expr_funcid(req)) + { + + case F_TIMEZONE_TEXT_TIMESTAMP: + to_utc = true; + pg_fallthrough; + case F_TIMEZONE_TEXT_TIMESTAMPTZ: + tzp = get_const_timezone_arg(args, 0); + break; + case F_TIMEZONE_TIMESTAMP: + to_utc = true; + pg_fallthrough; + case F_TIMEZONE_TIMESTAMPTZ: + tzp = session_timezone; + break; + + default: + PG_RETURN_POINTER(NULL); + } + + if (tzp == NULL || !pg_timezone_is_monotonic(tzp, TZ_GAP_SECOND, to_utc)) + PG_RETURN_POINTER(NULL); + + /* + * We need MONOTONICFUNC_INCREASING for either the first or + * second argument, but the other argument is either a constant + * or missing, so we can simply return MONOTONICFUNC_INCREASING + * for both. + */ + return monotonic_slope_support(req, 2, asc_slope); +} + +/* + * Prosupport for date_trunc(...) overloads with timestamp types. + */ +Datum +date_trunc_slope_support(PG_FUNCTION_ARGS) +{ + Const *unit_arg; + pg_tz *tzp = NULL; + SLOPE_REQUEST_ARGS(req, args, 2); + + unit_arg = (Const *) linitial(args); + if (!IsA(unit_arg, Const) || unit_arg->constisnull) + PG_RETURN_POINTER(NULL); + + switch (get_monotonic_expr_funcid(req)) + { + case F_DATE_TRUNC_TEXT_TIMESTAMP: + tzp = DecodeTimezoneNameToTz("UTC"); + break; + + case F_DATE_TRUNC_TEXT_TIMESTAMPTZ_TEXT: + tzp = get_const_timezone_arg(args, 2); + break; + + case F_DATE_TRUNC_TEXT_TIMESTAMPTZ: + tzp = session_timezone; + break; + + default: + PG_RETURN_POINTER(NULL); + } + + if (tzp == NULL) + PG_RETURN_POINTER(NULL); + + return timestamptz_support_lookup(req, unit_arg->constvalue, tzp); +} diff --git a/src/include/c.h b/src/include/c.h index 20cfbac54e7..6328dd1a39a 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -1285,6 +1285,9 @@ typedef struct PGAlignedXLogBlock PGAlignedXLogBlock; /* msb for char */ #define HIGHBIT (0x80) #define IS_HIGHBIT_SET(ch) ((unsigned char)(ch) & HIGHBIT) +/* ____-___ */ #define BITWISE_HI_AT(n) (1llu << n) +/* _____--- */ #define BITWISE_HI_RIGHT(n) ((1llu << (n + 1)) - 1) +/* -----___ */ #define BITWISE_LO_RIGHT(n) ~BIT_MASK_HI_RIGHT(n) /* * Support macros for escaping strings. escape_backslash should be true @@ -1565,6 +1568,8 @@ typedef uint32_t char32_t; #endif #endif + + /* IWYU pragma: end_exports */ #endif /* C_H */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 6adf4cc9c19..9e7d8f2e6bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -2471,10 +2471,12 @@ prorettype => 'timestamptz', proargtypes => 'float8', prosrc => 'float8_timestamptz' }, { oid => '1159', descr => 'adjust timestamp to new time zone', - proname => 'timezone', prorettype => 'timestamp', + proname => 'timezone', prosupport => 'timezone_prosupport', + prorettype => 'timestamp', proargtypes => 'text timestamptz', prosrc => 'timestamptz_zone' }, { oid => '6334', descr => 'adjust timestamp to local time zone', - proname => 'timezone', provolatile => 's', prorettype => 'timestamp', + proname => 'timezone', prosupport => 'timezone_prosupport', + provolatile => 's', prorettype => 'timestamp', proargtypes => 'timestamptz', prosrc => 'timestamptz_at_local' }, { oid => '1160', descr => 'I/O', @@ -2552,6 +2554,7 @@ prosrc => 'see system_functions.sql' }, { oid => '1178', descr => 'convert timestamp with time zone to date', proname => 'date', provolatile => 's', prorettype => 'date', + prosupport => 'timestamptz_date_slope_support', proargtypes => 'timestamptz', prosrc => 'timestamptz_date' }, { oid => '1181', descr => 'age of a transaction ID, in transactions before current transaction', @@ -2635,10 +2638,12 @@ { oid => '1217', descr => 'truncate timestamp with time zone to specified units', proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz', + prosupport => 'date_trunc_slope_support', proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' }, { oid => '1284', descr => 'truncate timestamp with time zone to specified units in specified time zone', proname => 'date_trunc', prorettype => 'timestamptz', + prosupport => 'date_trunc_slope_support', proargtypes => 'text timestamptz text', prosrc => 'timestamptz_trunc_zone' }, { oid => '1218', descr => 'truncate interval to specified units', proname => 'date_trunc', prorettype => 'interval', @@ -6573,7 +6578,7 @@ proname => 'time', provolatile => 's', prorettype => 'time', proargtypes => 'timestamptz', prosrc => 'timestamptz_time' }, { oid => '2020', descr => 'truncate timestamp to specified units', - proname => 'date_trunc', prosupport => 'arg1_asc_slope_support', + proname => 'date_trunc', prosupport => 'date_trunc_slope_support', prorettype => 'timestamp', proargtypes => 'text timestamp', prosrc => 'timestamp_trunc' }, { oid => '6177', descr => 'bin timestamp into specified interval', @@ -6736,12 +6741,14 @@ { oid => '2069', descr => 'adjust timestamp to new time zone', proname => 'timezone', prorettype => 'timestamptz', + prosupport => 'timezone_prosupport', proargtypes => 'text timestamp', prosrc => 'timestamp_zone' }, { oid => '2070', descr => 'adjust timestamp to new time zone', proname => 'timezone', prorettype => 'timestamptz', proargtypes => 'interval timestamp', prosrc => 'timestamp_izone' }, { oid => '6335', descr => 'adjust timestamp to local time zone', proname => 'timezone', provolatile => 's', prorettype => 'timestamptz', + prosupport => 'timezone_prosupport', proargtypes => 'timestamp', prosrc => 'timestamp_at_local' }, { oid => '2071', proname => 'date_pl_interval', @@ -12865,6 +12872,15 @@ { oid => '9960', descr => 'planner support for divide slope (sign-dependent)', proname => 'divide_slope_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'divide_slope_support' }, +{ oid => '9961', descr => 'planner support date(timestamptz)', + proname => 'timestamptz_date_slope_support', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'timestamptz_date_slope_support' }, +{ oid => '9962', descr => 'planner support date_trunc(...)', + proname => 'date_trunc_slope_support', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'date_trunc_slope_support' }, +{ oid => '9965', descr => 'planner support timezone(...)', + proname => 'timezone_prosupport', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'timezone_prosupport' } # AIO related functions { oid => '6399', descr => 'information about in-progress asynchronous IOs', diff --git a/src/include/pgtime.h b/src/include/pgtime.h index ba6705f71bd..14d077aa467 100644 --- a/src/include/pgtime.h +++ b/src/include/pgtime.h @@ -46,6 +46,23 @@ struct pg_tm const char *tm_zone; }; +/* + * These values are used to encode the gaps when in when converting + * timestamp representations between timezones across a time change, + * the most common case being DST. If a time change affects + * only minutes and seconds, would be represented using bits + * TZ_GAP_MINUTE and TZ_GAP_SECOND. + */ +typedef enum TZGapEffectBits { + TZ_GAP_YEAR , + TZ_GAP_MONTH , + TZ_GAP_DAY , + TZ_GAP_HOUR , + TZ_GAP_MINUTE, + TZ_GAP_SECOND, + TZ_GAP_NUM_BITS +} TZMonotonicityBits; + /* These structs are opaque outside the timezone library */ typedef struct pg_tz pg_tz; typedef struct pg_tzenum pg_tzenum; @@ -56,6 +73,8 @@ typedef struct pg_tzenum pg_tzenum; /* these functions are in localtime.c */ extern struct pg_tm *pg_localtime(const pg_time_t *timep, const pg_tz *tz); +extern bool pg_timezone_is_monotonic(const pg_tz *tz, TZMonotonicityBits unit, bool to_utc); + extern struct pg_tm *pg_gmtime(const pg_time_t *timep); extern int pg_next_dst_boundary(const pg_time_t *timep, long int *before_gmtoff, diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out index ebc4bd9b9d2..21a13857d05 100644 --- a/src/test/regress/expected/slope.out +++ b/src/test/regress/expected/slope.out @@ -252,21 +252,6 @@ select date_trunc('day', ts), count(*) from src group by 1; Output: date_trunc('day'::text, ts) (5 rows) --- date_trunc on timestamptz should not use index -explain (costs off, verbose) -select date_trunc('day', tstz), count(*) from src group by 1; - QUERY PLAN -------------------------------------------------------------- - GroupAggregate - Output: (date_trunc('day'::text, tstz)), count(*) - Group Key: (date_trunc('day'::text, src.tstz)) - -> Sort - Output: (date_trunc('day'::text, tstz)) - Sort Key: (date_trunc('day'::text, src.tstz)) - -> Index Only Scan using src_tstz_idx on slope.src - Output: date_trunc('day'::text, tstz) -(8 rows) - -- -- Test arithmetic operations -- @@ -425,23 +410,6 @@ select -v_int4 from src order by 1; Output: (- v_int4) (5 rows) --- --- Group and order --- -explain (costs off, verbose) -select tstz::date, count(*) from src group by 1 order by 1; - QUERY PLAN -------------------------------------------------------------- - GroupAggregate - Output: ((tstz)::date), count(*) - Group Key: ((src.tstz)::date) - -> Sort - Output: ((tstz)::date) - Sort Key: ((src.tstz)::date) - -> Index Only Scan using src_tstz_idx on slope.src - Output: (tstz)::date -(8 rows) - -- -- Test nested monotonic function -- @@ -844,8 +812,195 @@ SELECT v_int4::oid FROM src ORDER BY 1; -> Index Only Scan using src_v_int4_idx on src (3 rows) +-- +-- Now that some plans were shown, and we see that in many cases what +-- we care is whether a plan has a index scan or not. This function will +-- check that for us and just return +-- +CREATE OR REPLACE FUNCTION index_plan (query text) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +DECLARE + plan json; +BEGIN + EXECUTE 'EXPLAIN (COSTS OFF, FORMAT JSON) SELECT ' || query || ' ORDER BY 1' into plan; + RETURN plan->0->'Plan'->>'Node Type' LIKE 'Index%Scan'; +END +$$; +-- +-- Functions that take a timezone argument +-- +PREPARE query(text) AS +WITH +cols AS ( + SELECT * FROM unnest(ARRAY['ts', 'tstz']) + WITH ORDINALITY AS c(col, ord) +), +units AS ( + SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second']) + WITH ORDINALITY AS s(unit, ord) +) +SELECT + f AS expression, + coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER ( + WHERE index_plan(format('timezone(%L, %I) FROM src', $1, col)) + ), 'none') AS monotonic +FROM cols, + unnest(ARRAY['timezone(TZ, <>)']) AS f(f) +GROUP BY f +UNION ALL +SELECT + 'date_trunc(<>, tstz, TZ)' AS expression, + coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER ( + WHERE index_plan(format('date_trunc(%L, tstz, %L) FROM src', unit, $1)) + ), 'none') AS monotonic +FROM units +ORDER BY 1; +EXECUTE query('UTC'); + expression | monotonic +--------------------------+---------------------------------------- + date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second + timezone(TZ, <>) | ts, tstz +(2 rows) + +EXECUTE query('Africa/Ouagadougou'); + expression | monotonic +--------------------------+---------------------------------------- + date_trunc(<>, tstz, TZ) | year, month, day, hour, minute, second + timezone(TZ, <>) | tstz +(2 rows) + +EXECUTE query('Europe/London'); + expression | monotonic +--------------------------+------------------------ + date_trunc(<>, tstz, TZ) | year, month, day, hour + timezone(TZ, <>) | none +(2 rows) + +EXECUTE query('Antarctica/Troll'); + expression | monotonic +--------------------------+------------------ + date_trunc(<>, tstz, TZ) | year, month, day + timezone(TZ, <>) | none +(2 rows) + +EXECUTE query('Pacific/Guam'); + expression | monotonic +--------------------------+------------- + date_trunc(<>, tstz, TZ) | year, month + timezone(TZ, <>) | none +(2 rows) + +EXECUTE query('America/Goose_Bay'); + expression | monotonic +--------------------------+----------- + date_trunc(<>, tstz, TZ) | year + timezone(TZ, <>) | none +(2 rows) + +-- +-- Functions that depend on session timezone +-- +PREPARE query_local AS +WITH +cols AS ( + SELECT * FROM unnest(ARRAY['ts', 'tstz']) + WITH ORDINALITY AS c(col, ord) +), +units AS ( + SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second']) + WITH ORDINALITY AS s(unit, ord) +) +SELECT + format(f, '<>') AS expression, + coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER ( + WHERE index_plan(format(f, col) || ' FROM src') + ), 'none') AS monotonic +FROM cols, + unnest(ARRAY['%s AT LOCAL', 'timezone(%s)', '%s::date']) AS f(f) +GROUP BY f +UNION ALL +SELECT + format('date_trunc(<>, %s)', col) AS expression, + coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER ( + WHERE index_plan(format('date_trunc(%L, %I) FROM src', unit, col)) + ), 'none') AS monotonic +FROM cols, units +GROUP BY col +ORDER BY 1; +-- UTC +SET timezone = 'UTC'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | ts, tstz + <>::date | ts, tstz + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year, month, day, hour, minute, second + timezone(<>) | ts, tstz +(5 rows) + +-- No DST at all +SET timezone = 'Africa/Ouagadougou'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | tstz + <>::date | ts, tstz + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year, month, day, hour, minute, second + timezone(<>) | tstz +(5 rows) + +-- The common case: one hour DST +SET timezone = 'Europe/London'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | none + <>::date | ts, tstz + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year, month, day, hour + timezone(<>) | none +(5 rows) + +-- DST might affect the hour +SET timezone = 'Antarctica/Troll'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | none + <>::date | ts, tstz + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year, month, day + timezone(<>) | none +(5 rows) + +-- A weird case: used to go back a day +SET timezone = 'Pacific/Guam'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | none + <>::date | ts + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year, month + timezone(<>) | none +(5 rows) + +-- Even weirder, went back a month +SET timezone = 'America/Goose_Bay'; +EXECUTE query_local; + expression | monotonic +----------------------+---------------------------------------- + <> AT LOCAL | none + <>::date | ts + date_trunc(<>, ts) | year, month, day, hour, minute, second + date_trunc(<>, tstz) | year + timezone(<>) | none +(5 rows) + +DEALLOCATE ALL; DROP SCHEMA slope CASCADE; -NOTICE: drop cascades to 14 other objects +NOTICE: drop cascades to 15 other objects DETAIL: drop cascades to table t drop cascades to table u drop cascades to operator family test_int4_ops for access method btree @@ -860,3 +1015,4 @@ drop cascades to type non42 drop cascades to type fp_real drop cascades to table numeric_corners drop cascades to table inc +drop cascades to function index_plan(text) diff --git a/src/test/regress/expected/slope_catalog.out b/src/test/regress/expected/slope_catalog.out index 670f711466e..40357be8d1f 100644 --- a/src/test/regress/expected/slope_catalog.out +++ b/src/test/regress/expected/slope_catalog.out @@ -12,6 +12,7 @@ FROM pg_operator o JOIN pg_proc p ON p.oid = o.oprcode JOIN pg_proc sp ON sp.oid = p.prosupport WHERE sp.proname LIKE '%slope%' + OR sp.proname LIKE '%prosupport' ORDER BY sp.proname, o.oprname, left_type, right_type; oid | operator | left_type | right_type | prosupport ------+----------+-----------------------------+-----------------------------+------------------------- @@ -130,61 +131,68 @@ SELECT sp.proname AS prosupport FROM pg_proc p JOIN pg_proc sp ON sp.oid = p.prosupport -WHERE sp.proname LIKE '%slope%' +WHERE (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport') AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = p.oid) ORDER BY sp.proname, p.proname, arguments; - oid | function | arguments | returns | prosupport -------+--------------+-----------------------------------+-----------------------------+------------------------- - 2025 | timestamp | date, time without time zone | timestamp without time zone | addition_slope_support - 2466 | acosh | double precision | double precision | arg0_asc_slope_support - 1600 | asin | double precision | double precision | arg0_asc_slope_support - 2731 | asind | double precision | double precision | arg0_asc_slope_support - 2465 | asinh | double precision | double precision | arg0_asc_slope_support - 1602 | atan | double precision | double precision | arg0_asc_slope_support - 2733 | atand | double precision | double precision | arg0_asc_slope_support - 2467 | atanh | double precision | double precision | arg0_asc_slope_support - 1345 | cbrt | double precision | double precision | arg0_asc_slope_support - 2308 | ceil | double precision | double precision | arg0_asc_slope_support - 1711 | ceil | numeric | numeric | arg0_asc_slope_support - 2320 | ceiling | double precision | double precision | arg0_asc_slope_support - 2167 | ceiling | numeric | numeric | arg0_asc_slope_support - 2029 | date | timestamp without time zone | date | arg0_asc_slope_support - 1608 | degrees | double precision | double precision | arg0_asc_slope_support - 6219 | erf | double precision | double precision | arg0_asc_slope_support - 1347 | exp | double precision | double precision | arg0_asc_slope_support - 1732 | exp | numeric | numeric | arg0_asc_slope_support - 2309 | floor | double precision | double precision | arg0_asc_slope_support - 1712 | floor | numeric | numeric | arg0_asc_slope_support - 1341 | ln | double precision | double precision | arg0_asc_slope_support - 1734 | ln | numeric | numeric | arg0_asc_slope_support - 1340 | log | double precision | double precision | arg0_asc_slope_support - 1741 | log | numeric | numeric | arg0_asc_slope_support - 1194 | log10 | double precision | double precision | arg0_asc_slope_support - 1481 | log10 | numeric | numeric | arg0_asc_slope_support - 1733 | numeric_exp | numeric | numeric | arg0_asc_slope_support - 1735 | numeric_ln | numeric | numeric | arg0_asc_slope_support - 1731 | numeric_sqrt | numeric | numeric | arg0_asc_slope_support - 1609 | radians | double precision | double precision | arg0_asc_slope_support - 1342 | round | double precision | double precision | arg0_asc_slope_support - 1708 | round | numeric | numeric | arg0_asc_slope_support - 1707 | round | numeric, integer | numeric | arg0_asc_slope_support - 2462 | sinh | double precision | double precision | arg0_asc_slope_support - 1344 | sqrt | double precision | double precision | arg0_asc_slope_support - 1730 | sqrt | numeric | numeric | arg0_asc_slope_support - 2464 | tanh | double precision | double precision | arg0_asc_slope_support - 2024 | timestamp | date | timestamp without time zone | arg0_asc_slope_support - 1174 | timestamptz | date | timestamp with time zone | arg0_asc_slope_support - 1359 | timestamptz | date, time with time zone | timestamp with time zone | arg0_asc_slope_support - 1176 | timestamptz | date, time without time zone | timestamp with time zone | arg0_asc_slope_support - 1158 | to_timestamp | double precision | timestamp with time zone | arg0_asc_slope_support - 1343 | trunc | double precision | double precision | arg0_asc_slope_support - 1710 | trunc | numeric | numeric | arg0_asc_slope_support - 1709 | trunc | numeric, integer | numeric | arg0_asc_slope_support - 1601 | acos | double precision | double precision | arg0_desc_slope_support - 2732 | acosd | double precision | double precision | arg0_desc_slope_support - 6220 | erfc | double precision | double precision | arg0_desc_slope_support - 2020 | date_trunc | text, timestamp without time zone | timestamp without time zone | arg1_asc_slope_support -(49 rows) + oid | function | arguments | returns | prosupport +------+--------------+--------------------------------------+-----------------------------+-------------------------------- + 2025 | timestamp | date, time without time zone | timestamp without time zone | addition_slope_support + 2466 | acosh | double precision | double precision | arg0_asc_slope_support + 1600 | asin | double precision | double precision | arg0_asc_slope_support + 2731 | asind | double precision | double precision | arg0_asc_slope_support + 2465 | asinh | double precision | double precision | arg0_asc_slope_support + 1602 | atan | double precision | double precision | arg0_asc_slope_support + 2733 | atand | double precision | double precision | arg0_asc_slope_support + 2467 | atanh | double precision | double precision | arg0_asc_slope_support + 1345 | cbrt | double precision | double precision | arg0_asc_slope_support + 2308 | ceil | double precision | double precision | arg0_asc_slope_support + 1711 | ceil | numeric | numeric | arg0_asc_slope_support + 2320 | ceiling | double precision | double precision | arg0_asc_slope_support + 2167 | ceiling | numeric | numeric | arg0_asc_slope_support + 2029 | date | timestamp without time zone | date | arg0_asc_slope_support + 1608 | degrees | double precision | double precision | arg0_asc_slope_support + 6219 | erf | double precision | double precision | arg0_asc_slope_support + 1347 | exp | double precision | double precision | arg0_asc_slope_support + 1732 | exp | numeric | numeric | arg0_asc_slope_support + 2309 | floor | double precision | double precision | arg0_asc_slope_support + 1712 | floor | numeric | numeric | arg0_asc_slope_support + 1341 | ln | double precision | double precision | arg0_asc_slope_support + 1734 | ln | numeric | numeric | arg0_asc_slope_support + 1340 | log | double precision | double precision | arg0_asc_slope_support + 1741 | log | numeric | numeric | arg0_asc_slope_support + 1194 | log10 | double precision | double precision | arg0_asc_slope_support + 1481 | log10 | numeric | numeric | arg0_asc_slope_support + 1733 | numeric_exp | numeric | numeric | arg0_asc_slope_support + 1735 | numeric_ln | numeric | numeric | arg0_asc_slope_support + 1731 | numeric_sqrt | numeric | numeric | arg0_asc_slope_support + 1609 | radians | double precision | double precision | arg0_asc_slope_support + 1342 | round | double precision | double precision | arg0_asc_slope_support + 1708 | round | numeric | numeric | arg0_asc_slope_support + 1707 | round | numeric, integer | numeric | arg0_asc_slope_support + 2462 | sinh | double precision | double precision | arg0_asc_slope_support + 1344 | sqrt | double precision | double precision | arg0_asc_slope_support + 1730 | sqrt | numeric | numeric | arg0_asc_slope_support + 2464 | tanh | double precision | double precision | arg0_asc_slope_support + 2024 | timestamp | date | timestamp without time zone | arg0_asc_slope_support + 1174 | timestamptz | date | timestamp with time zone | arg0_asc_slope_support + 1359 | timestamptz | date, time with time zone | timestamp with time zone | arg0_asc_slope_support + 1176 | timestamptz | date, time without time zone | timestamp with time zone | arg0_asc_slope_support + 1158 | to_timestamp | double precision | timestamp with time zone | arg0_asc_slope_support + 1343 | trunc | double precision | double precision | arg0_asc_slope_support + 1710 | trunc | numeric | numeric | arg0_asc_slope_support + 1709 | trunc | numeric, integer | numeric | arg0_asc_slope_support + 1601 | acos | double precision | double precision | arg0_desc_slope_support + 2732 | acosd | double precision | double precision | arg0_desc_slope_support + 6220 | erfc | double precision | double precision | arg0_desc_slope_support + 1217 | date_trunc | text, timestamp with time zone | timestamp with time zone | date_trunc_slope_support + 1284 | date_trunc | text, timestamp with time zone, text | timestamp with time zone | date_trunc_slope_support + 2020 | date_trunc | text, timestamp without time zone | timestamp without time zone | date_trunc_slope_support + 1178 | date | timestamp with time zone | date | timestamptz_date_slope_support + 1159 | timezone | text, timestamp with time zone | timestamp without time zone | timezone_prosupport + 2069 | timezone | text, timestamp without time zone | timestamp with time zone | timezone_prosupport + 6334 | timezone | timestamp with time zone | timestamp without time zone | timezone_prosupport + 6335 | timezone | timestamp without time zone | timestamp with time zone | timezone_prosupport +(56 rows) -- Operators whose name has slope support for some types but not others SELECT @@ -196,14 +204,15 @@ FROM pg_operator u JOIN pg_proc up ON up.oid = u.oprcode WHERE (up.prosupport = 0 OR NOT EXISTS ( SELECT 1 FROM pg_proc sp - WHERE sp.oid = up.prosupport AND sp.proname LIKE '%slope%')) + WHERE sp.oid = up.prosupport + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport'))) AND EXISTS ( SELECT 1 FROM pg_operator s JOIN pg_proc sp_impl ON sp_impl.oid = s.oprcode JOIN pg_proc sp_sup ON sp_sup.oid = sp_impl.prosupport WHERE s.oprname = u.oprname - AND sp_sup.proname LIKE '%slope%') + AND (sp_sup.proname LIKE '%slope%' OR sp_sup.proname LIKE '%prosupport')) ORDER BY u.oprname, left_type, right_type; oid | operator | left_type | right_type ------+----------+-----------------------------+----------------------------- @@ -268,29 +277,32 @@ SELECT FROM pg_proc u WHERE (u.prosupport = 0 OR NOT EXISTS ( SELECT 1 FROM pg_proc sp - WHERE sp.oid = u.prosupport AND sp.proname LIKE '%slope%')) + WHERE sp.oid = u.prosupport + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport'))) AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = u.oid) AND EXISTS ( SELECT 1 FROM pg_proc s JOIN pg_proc sp ON sp.oid = s.prosupport WHERE s.proname = u.proname - AND sp.proname LIKE '%slope%' + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport') AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = s.oid)) ORDER BY u.proname, arguments; - oid | function | arguments | returns -------+--------------+--------------------------------------+----------------------------- - 1178 | date | timestamp with time zone | date - 1218 | date_trunc | text, interval | interval - 1217 | date_trunc | text, timestamp with time zone | timestamp with time zone - 1284 | date_trunc | text, timestamp with time zone, text | timestamp with time zone - 1736 | log | numeric, numeric | numeric - 2027 | timestamp | timestamp with time zone | timestamp without time zone - 1961 | timestamp | timestamp without time zone, integer | timestamp without time zone - 1967 | timestamptz | timestamp with time zone, integer | timestamp with time zone - 2028 | timestamptz | timestamp without time zone | timestamp with time zone - 1778 | to_timestamp | text, text | timestamp with time zone - 753 | trunc | macaddr | macaddr - 4112 | trunc | macaddr8 | macaddr8 -(12 rows) + oid | function | arguments | returns +------+--------------+---------------------------------------+----------------------------- + 1218 | date_trunc | text, interval | interval + 1736 | log | numeric, numeric | numeric + 2027 | timestamp | timestamp with time zone | timestamp without time zone + 1961 | timestamp | timestamp without time zone, integer | timestamp without time zone + 1967 | timestamptz | timestamp with time zone, integer | timestamp with time zone + 2028 | timestamptz | timestamp without time zone | timestamp with time zone + 2038 | timezone | interval, time with time zone | time with time zone + 1026 | timezone | interval, timestamp with time zone | timestamp without time zone + 2070 | timezone | interval, timestamp without time zone | timestamp with time zone + 2037 | timezone | text, time with time zone | time with time zone + 6336 | timezone | time with time zone | time with time zone + 1778 | to_timestamp | text, text | timestamp with time zone + 753 | trunc | macaddr | macaddr + 4112 | trunc | macaddr8 | macaddr8 +(14 rows) diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql index 1defff4d955..7e8b85b9ec3 100644 --- a/src/test/regress/sql/slope.sql +++ b/src/test/regress/sql/slope.sql @@ -178,10 +178,6 @@ select ts::date, count(*) from src group by 1; explain (costs off, verbose) select date_trunc('day', ts), count(*) from src group by 1; --- date_trunc on timestamptz should not use index -explain (costs off, verbose) -select date_trunc('day', tstz), count(*) from src group by 1; - -- -- Test arithmetic operations @@ -203,7 +199,6 @@ select v_int4 * 2, count(*) from src group by 1; explain (costs off, verbose) select v_int4 / 2, count(*) from src group by 1; - -- -- Test decreasing functions -- These queries can't use the index order because group pathkeys @@ -248,13 +243,6 @@ select -v_int4 from src order by 1 desc; explain (costs off, verbose) select -v_int4 from src order by 1; --- --- Group and order --- - -explain (costs off, verbose) -select tstz::date, count(*) from src group by 1 order by 1; - -- -- Test nested monotonic function -- @@ -536,4 +524,115 @@ from src; EXPLAIN (COSTS OFF) SELECT v_int4::oid FROM src ORDER BY 1; + + +-- +-- Now that some plans were shown, and we see that in many cases what +-- we care is whether a plan has a index scan or not. This function will +-- check that for us and just return +-- +CREATE OR REPLACE FUNCTION index_plan (query text) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ +DECLARE + plan json; +BEGIN + EXECUTE 'EXPLAIN (COSTS OFF, FORMAT JSON) SELECT ' || query || ' ORDER BY 1' into plan; + RETURN plan->0->'Plan'->>'Node Type' LIKE 'Index%Scan'; +END +$$; + + +-- +-- Functions that take a timezone argument +-- +PREPARE query(text) AS +WITH +cols AS ( + SELECT * FROM unnest(ARRAY['ts', 'tstz']) + WITH ORDINALITY AS c(col, ord) +), +units AS ( + SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second']) + WITH ORDINALITY AS s(unit, ord) +) +SELECT + f AS expression, + coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER ( + WHERE index_plan(format('timezone(%L, %I) FROM src', $1, col)) + ), 'none') AS monotonic +FROM cols, + unnest(ARRAY['timezone(TZ, <>)']) AS f(f) +GROUP BY f +UNION ALL +SELECT + 'date_trunc(<>, tstz, TZ)' AS expression, + coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER ( + WHERE index_plan(format('date_trunc(%L, tstz, %L) FROM src', unit, $1)) + ), 'none') AS monotonic +FROM units +ORDER BY 1; + +EXECUTE query('UTC'); +EXECUTE query('Africa/Ouagadougou'); +EXECUTE query('Europe/London'); +EXECUTE query('Antarctica/Troll'); +EXECUTE query('Pacific/Guam'); +EXECUTE query('America/Goose_Bay'); + +-- +-- Functions that depend on session timezone +-- +PREPARE query_local AS +WITH +cols AS ( + SELECT * FROM unnest(ARRAY['ts', 'tstz']) + WITH ORDINALITY AS c(col, ord) +), +units AS ( + SELECT * FROM unnest(ARRAY['year', 'month', 'day', 'hour', 'minute', 'second']) + WITH ORDINALITY AS s(unit, ord) +) +SELECT + format(f, '<>') AS expression, + coalesce(string_agg(col, ', ' ORDER BY cols.ord) FILTER ( + WHERE index_plan(format(f, col) || ' FROM src') + ), 'none') AS monotonic +FROM cols, + unnest(ARRAY['%s AT LOCAL', 'timezone(%s)', '%s::date']) AS f(f) +GROUP BY f +UNION ALL +SELECT + format('date_trunc(<>, %s)', col) AS expression, + coalesce(string_agg(unit, ', ' ORDER BY units.ord) FILTER ( + WHERE index_plan(format('date_trunc(%L, %I) FROM src', unit, col)) + ), 'none') AS monotonic +FROM cols, units +GROUP BY col +ORDER BY 1; + +-- UTC +SET timezone = 'UTC'; +EXECUTE query_local; + +-- No DST at all +SET timezone = 'Africa/Ouagadougou'; +EXECUTE query_local; + +-- The common case: one hour DST +SET timezone = 'Europe/London'; +EXECUTE query_local; + +-- DST might affect the hour +SET timezone = 'Antarctica/Troll'; +EXECUTE query_local; + +-- A weird case: used to go back a day +SET timezone = 'Pacific/Guam'; +EXECUTE query_local; + +-- Even weirder, went back a month +SET timezone = 'America/Goose_Bay'; +EXECUTE query_local; + + +DEALLOCATE ALL; DROP SCHEMA slope CASCADE; \ No newline at end of file diff --git a/src/test/regress/sql/slope_catalog.sql b/src/test/regress/sql/slope_catalog.sql index 8256cd232ab..79cb3666f84 100644 --- a/src/test/regress/sql/slope_catalog.sql +++ b/src/test/regress/sql/slope_catalog.sql @@ -13,6 +13,7 @@ FROM pg_operator o JOIN pg_proc p ON p.oid = o.oprcode JOIN pg_proc sp ON sp.oid = p.prosupport WHERE sp.proname LIKE '%slope%' + OR sp.proname LIKE '%prosupport' ORDER BY sp.proname, o.oprname, left_type, right_type; -- Functions (non-operator) with slope prosupport @@ -24,7 +25,7 @@ SELECT sp.proname AS prosupport FROM pg_proc p JOIN pg_proc sp ON sp.oid = p.prosupport -WHERE sp.proname LIKE '%slope%' +WHERE (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport') AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = p.oid) ORDER BY sp.proname, p.proname, arguments; @@ -38,14 +39,15 @@ FROM pg_operator u JOIN pg_proc up ON up.oid = u.oprcode WHERE (up.prosupport = 0 OR NOT EXISTS ( SELECT 1 FROM pg_proc sp - WHERE sp.oid = up.prosupport AND sp.proname LIKE '%slope%')) + WHERE sp.oid = up.prosupport + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport'))) AND EXISTS ( SELECT 1 FROM pg_operator s JOIN pg_proc sp_impl ON sp_impl.oid = s.oprcode JOIN pg_proc sp_sup ON sp_sup.oid = sp_impl.prosupport WHERE s.oprname = u.oprname - AND sp_sup.proname LIKE '%slope%') + AND (sp_sup.proname LIKE '%slope%' OR sp_sup.proname LIKE '%prosupport')) ORDER BY u.oprname, left_type, right_type; -- Functions whose name has slope support for some signatures but not others @@ -57,13 +59,14 @@ SELECT FROM pg_proc u WHERE (u.prosupport = 0 OR NOT EXISTS ( SELECT 1 FROM pg_proc sp - WHERE sp.oid = u.prosupport AND sp.proname LIKE '%slope%')) + WHERE sp.oid = u.prosupport + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport'))) AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = u.oid) AND EXISTS ( SELECT 1 FROM pg_proc s JOIN pg_proc sp ON sp.oid = s.prosupport WHERE s.proname = u.proname - AND sp.proname LIKE '%slope%' + AND (sp.proname LIKE '%slope%' OR sp.proname LIKE '%prosupport') AND NOT EXISTS (SELECT 1 FROM pg_operator o WHERE o.oprcode = s.oid)) ORDER BY u.proname, arguments; diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c index a3eb3bacb9b..5de2c0cd3e3 100644 --- a/src/timezone/localtime.c +++ b/src/timezone/localtime.c @@ -1769,6 +1769,21 @@ pg_next_dst_boundary(const pg_time_t *timep, return 1; } +/* + * Determine whether timestamps truncated to the specified + * unit is monotonic when either converting from UTC to TZ + * or from TZ to UTC, the last argument indicates the direction + * if the conversion of interest. + */ +bool +pg_timezone_is_monotonic(const pg_tz *tz, TZMonotonicityBits unit, bool to_utc) +{ + return ((to_utc ? + tz->state.monotonicity.utc : + tz->state.monotonicity.local + ) & BITWISE_HI_AT(unit)) != 0; +} + /* * Identify a timezone abbreviation's meaning in the given zone * diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c index 9561d94a67d..6725a460459 100644 --- a/src/timezone/pgtz.c +++ b/src/timezone/pgtz.c @@ -216,6 +216,165 @@ init_timezone_hashtable(void) return true; } +static void +get_tz_change_gaps(struct pg_tm *tm_before, struct pg_tm *tm_after, short *result) +{ + #define mark(field, unit) \ + do { \ + if (tm_before->tm_##field > tm_after->tm_##field) \ + { \ + if (TZ_GAP_##unit == 0) \ + *result = 0; \ + else \ + *result &= BITWISE_HI_RIGHT(TZ_GAP_##unit - 1); \ + } \ + else if (tm_before->tm_##field < tm_after->tm_##field) \ + return; \ + } while (0) + mark(year, YEAR); + mark(mon, MONTH); + mark(mday, DAY); + mark(hour, HOUR); + mark(min, MINUTE); + mark(sec, SECOND); + #undef mark +} + +/* + * Check every time transition that moves time backwards + * up to which part of the timestamp the effects are observable + * on a timestamp truncation fashion. This information will be stored + * in the timezone state to assist decisions related to monotonicity + * analysis. + * 2026-07-02 01:59:59.999999 to + * 2026-07-02 01:00:00.000000 + * ==== == == == << xx xxxxxx + * + * would not violate monotonicity for year, month, day, and hour + * but would break for minutes and seconds. + */ +static void +inspect_monotonicity(struct pg_tz *tz) +{ + struct state *sp; + struct pg_tm tm_before; + struct pg_tm tm_after; + struct pg_tm *p; + + int_fast32_t prev_offset; + int_fast32_t offset; + sp = &tz->state; + + if (sp == NULL || sp->typecnt <= 0){ + /* invalid input, conservative behaviour */ + tz->state.monotonicity.local = 0; + tz->state.monotonicity.utc = 0; + return; + } + /* + *start assuming monotonicity, update as we observe + * counter-examples below + */ + tz->state.monotonicity.local = BITWISE_HI_RIGHT(TZ_GAP_NUM_BITS); + tz->state.monotonicity.utc = BITWISE_HI_RIGHT(TZ_GAP_NUM_BITS); + if (sp->timecnt == 0) + { + return; + } + offset = sp->ttis[0].tt_utoff; + + for (int i = 0; i < sp->timecnt; i++) + { + pg_time_t t_before; + pg_time_t t_after; + + t_after = sp->ats[i]; + t_before = t_after - 1; + + prev_offset = offset; + offset = sp->ttis[sp->types[i]].tt_utoff; + /* + * The variables t_before and t_after are the last + * timestamp before the transition, and the first timestamp + * after the transition at UTC, with 1 microsecond resolution. + * + * The offset indicates the number of seconds the local time + * is ahead of GMT. + * + * Conceptually, + * + * tz_before = (t + prev_offset - 1 micro second) + * tz_after = (t + offset) + * + */ + if(offset < prev_offset) + { + /* + * at this transition we have offset < prev_offset, + * since offset is in seconds, + * offset <= prev_offset - 1 second + * and offset < prev_offset - 1 microsecond + * and thus + * t + offset < t + prev_offset - 1 microsecond + * i.e. tz_after < tz_before + * + * So, local time moves backwards, so fields affected + * by this transition stop being monotonic at the local time. + */ + p = pg_localtime(&t_before, tz); + if (p == NULL) + return; + tm_before = *p; + + p = pg_localtime(&t_after, tz); + if (p == NULL) + return; + tm_after = *p; + get_tz_change_gaps( + &tm_before, &tm_after, &tz->state.monotonicity.local + ); + } + else + { + /* + * Similarly, here tz_after jumps forward, while this doesn't + * impact the local time monotonicity, but creates a gap in + * the local time, since + * tz_after = tz_before + (offset - prev_offset) + 1 microsecond + * even if tz_before < t < tz_after never existed in that + * particular timezone, they might exist as postgres timestamp + * value. + * + * The documented behaviour is: + * An invalid timestamp that appears to fall within a jump-forward + * daylight savings transition is assigned the UTC offset that + * prevailed in the time zone just before the transition. + * + * so that means that when converting from local time at the given + * timezone back to UTC, the time will remain continuous tz_before + * to tz_after - 1, the discontinuity will happen at tz_after. + * so in this case we have + * + * utc_before = (utc_after - 1) + (offset - prev_offset) + */ + t_before += (offset - prev_offset) * USECS_PER_SEC; + p = pg_gmtime(&t_before); + if(p == NULL) + return; + tm_before = *p; + + p = pg_gmtime(&t_after); + if(p == NULL) + return; + tm_after = *p; + + get_tz_change_gaps( + &tm_before, &tm_after, &tz->state.monotonicity.utc + ); + } + } +} + /* * Load a timezone from file or from cache. * Does not verify that the timezone is acceptable! @@ -280,7 +439,7 @@ pg_tzset(const char *tzname) /* hash_search already copied uppername into the hash key */ strcpy(tzp->tz.TZname, canonname); memcpy(&tzp->tz.state, &tzstate, sizeof(tzstate)); - + inspect_monotonicity(&tzp->tz); return &tzp->tz; } diff --git a/src/timezone/pgtz.h b/src/timezone/pgtz.h index d4ae94de74a..235178da843 100644 --- a/src/timezone/pgtz.h +++ b/src/timezone/pgtz.h @@ -25,6 +25,7 @@ */ #define TZ_RUNTIME_LEAPS 1 + /* * Limit to time zone abbreviation length in proleptic TZ strings. * This is distinct from TZ_MAX_CHARS, which limits TZif file contents. @@ -60,6 +61,11 @@ struct lsinfo int_fast32_2s ls_corr; /* correction to apply */ }; +struct tz_gaps { + short local; + short utc; +}; + /* This abbreviation means local time is unspecified. */ static char const UNSPEC[] = "-00"; @@ -90,6 +96,7 @@ struct state int charcnt; bool goback; bool goahead; + struct tz_gaps monotonicity; pg_time_t ats[TZ_MAX_TIMES]; unsigned char types[TZ_MAX_TIMES]; struct ttinfo ttis[TZ_MAX_TYPES]; -- 2.53.0