Project: YugabyteDB (App Definition and Development / Database)
File: recommendation.md

title: Recommended practice for specifying the UTC offset [YSQL] headerTitle: Recommended practice for specifying the UTC offset linkTitle: Recommended practice description: Recommends a practice to for specifying the offset from the UTC Time Standard safely. [YSQL] menu: v2.18: identifier: recommendation parent: timezones weight: 80 type: docs {{< tip title="Write user-defined functions to wrap 'set timezone' and the overloads of the built-in function 'timezone()'." >}} Yugabyte recommends that you create two user-defined function overload sets, thus: settimezone(), with (text) and (interval) overloads to wrap set timezone_ to ensure that safe, approved arguments are used. attimezone() with (text, timestamp), (interval, timestamp), (text, timestamptz), (interval, timestamptz) overloads to wrap the corresponding overloads of the timezone()_ built-in function to ensure that safe, approved arguments are used. "Safe, approved arguments" means: When a timezone is specified using its name, this is checked against a list of approved namesa subset of the rows in pgtimezonenames.name When a timezone is specified using an interval value, this is checked to ensure that it lies in the range defined by the overall maximum and minimum values of utcoffset columns in the pgtimezonenames and pgtimezoneabbrevs catalog views. It's also checked to ensure that it's an integral multiple of fifteen minutes, respecting the convention followed by every timezone shown by pgtimezonenames_. Following these recommendations protects you from the many opportunities to go wrong brought by using the native functionality with no constraints; and yet doing so still allows you all the functionality that you could need. {{< /tip >}}</br> {{< tip title="Download and install the date-time utilities code." >}} The code on this page depends on the . It also depends on the code. And this, in turn, depends on the . These components are all included in the larger that includes all of the reusable code that the overall section describes and uses. The code on that this page defines is intended for reuse. It, too, is therefore included in the date-time utilities downloadable code kit. {{< /tip >}} This is the union of the view, the view, and the single row that specifies the facts about the UTC Time

```plpgslq drop view if exists approvedtimezonenames cascade; create view approvedtimezonenames as select name, std_abbrev, dst_abbrev, std_offset, dst_offset from extendedtimezonenames where name = 'UTC' or ( lower(status) = 'canonical' and country_code is not null and country_code <> '' and lat_long is not null and lat_long <> '' and name not like '%0%' and name not like '%1%' and name not like '%2%' and name not like '%3%' and name not like '%4%' and name not like '%5%' and name not like '%6%' and name not like '%7%' and name not like '%8%' and name not like '%9%' and lower(name) not in (select lower(abbrev) from pgtimezonenames) and lower(name) not in (select lower(abbrev) from pgtimezoneabbrevs) ); ``` These two "assert" procedures are used by both the and the user-defined function overloads. They depend upon some code described in the section . And these, in turn, depend on some code described in the section. ```plpgsql drop procedure if exists assertapprovedtimezone_name(text) cascade; create procedure assertapprovedtimezone_name(tz in text) language plpgsql as $body$ declare bad constant boolean not null := (select count(*) from approvedtimezonenames where lower(name) = lower(tz)) <> 1; begin if bad then declare code constant text not null := '22023'; msg constant text not null := 'Invalid value for parameter TimeZone "'||tz||'"'; hint constant text not null := 'Use a name that''s found exactly once in "approvedtimezonenames"'; begin raise exception using errcode = code, message = msg, hint = hint; end; end if; end; $body$; ``` ```plpgsql drop procedure if exists assertacceptabletimezone_interval(interval) cascade; create procedure assertacceptabletimezone_interval(i in interval) language plpgsql as $body$ declare minutcoffset constant interval not null := ( select least( (select min(utcoffset) from pgtimezone_names), (select min(utcoffset) from pgtimezone_abbrevs) ) ); maxutcoffset constant interval not null := ( select greatest( (select max(utcoffset) from pgtimezone_names), (select max(utcoffset) from pgtimezone_abbrevs) ) ); -- Check that the values are "pure seconds" intervals. mini constant intervalsecondst not null := minutc_offset; maxi constant intervalsecondst not null := maxutc_offset; -- The interval value must not have a seconds component. bad constant boolean not null := not( (i between mini and maxi) and (extract(seconds from i) = 0.0) ); begin if bad then declare code constant text not null := '22023'; msg constant text not null := 'Invalid value for interval: "'||i::text||'"'; hint constant text not null := 'Use a value between "'||mini||'" and "'||maxi||'" with seconds cpt = zero'; begin raise exception using errcode = code, message = msg, hint = hint; end; end if; end; $body$; ``` Should you be concerned about the performance of this check, you can rely on the fact that the limits for acceptable interval values that it discovers on every invocation can simply be declared as constants in the functions source code. The safest way to do this is to write a generator procedure to create [or replace] the assertacceptabletimezoneinterval() procedure and to document the practice that requires that this generator be run whenever the YugabyteDB version (or the PostgreSQL version) is created or changed. (Your practice rule would need to be stated more carefully if you allow changes to the configuration files that determine the contents that the pgtimezonenames view and the pgtimezoneabbrevs_ view

```plpgsql drop procedure if exists set_timezone(text) cascade; create procedure set_timezone(tz in text) language plpgsql as $body$ begin call assertapprovedtimezone_name(tz); declare stmt constant text not null := 'set timezone = '''||tz||''''; begin execute stmt; end; end; $body$; drop procedure if exists set_timezone(interval) cascade; create procedure set_timezone(i in interval) language plpgsql as $body$ begin call assertacceptabletimezone_interval(i); declare stmt constant text not null := 'set time zone interval '''||i::text||''''; begin execute stmt; end; end; $body$; ``` Do this to see the overloads of interest of the timezone() built-in function. ```plpgsql \df timezone() ``` This is the result: ```output Result data type | Argument data types --+-- ... timestamp without time zone | interval, timestamp with time zone timestamp with time zone | interval, timestamp without time zone ... timestamp without time zone | text, timestamp with time zone timestamp with time zone | text, timestamp without time zone ``` (The output also lists overloads for the timetz data type. This has been elided because, following the , Yugabyte recommends that you don't use this data type.) Create wrapper functions for the four listed built-in functions: ```plpgsql -- plain timestamp in, timestamptz out. drop function if exists at_timezone(text, timestamp) cascade; create function at_timezone(tz in text, t in timestamp) returns timestamptz language plpgsql as $body$ begin call assertapprovedtimezone_name(tz); return timezone(tz, t); end; $body$; -- This overload is almost textually identical to the preceding one. -- The data types of the second formal and the return have -- simply been exchanged. -- timestamptz in, plain timestamp out. drop function if exists at_timezone(text, timestamptz) cascade; create function at_timezone(tz in text, t in timestamptz) returns timestamp language plpgsql as $body$ begin call assertapprovedtimezone_name(tz); return timezone(tz, t); end; $body$; -- interval in, timestamptz out. drop function if exists at_timezone(interval, timestamp) cascade; create function at_timezone(i in interval, t in timestamp) returns timestamptz language plpgsql as $body$ begin call assertacceptabletimezone_interval(i); return timezone(i, t); end; $body$; -- This overload is almost textually identical to the preceding one. -- The data types of the second formal and the return have -- simply been exchanged. -- interval in, plain timestamp out. drop function if exists at_timezone(interval, timestamptz) cascade; create function at_timezone(i in interval, t in timestamptz) returns timestamp language plpgsql as $body$ begin call assertacceptabletimezone_interval(i); return timezone(i, t); end; $body$; ``` The following tests contain some commented out raise info statements. They show the error messages that you get when you supply a timezone name that isn't approved or, or an interval value that isn't

Do this: ```plpgsql do $body$ declare tz_in text not null := ''; tz_out text not null := ''; good_zones constant text[] := array[ 'UTC', 'Asia/Kathmandu', 'Europe/Amsterdam']; begin foreach tzin in array goodzones loop call settimezone(tzin); show timezone into tz_out; declare msg constant text not null := tz_in||' assert failed'; begin assert tzout = tzin, msg; end; end loop; begin call set_timezone('Bad'); assert false, 'Logic error'; exception when invalidparametervalue then declare msg text not null := ''; hint text not null := ''; begin get stacked diagnostics msg = message_text, hint = pgexceptionhint; /* raise info '%', msg; raise info '%', hint; */ end; end; end; $body$; ``` The block finishes silently, showing that all of the assertions hold. Uncomment the raise info statements and repeat the test. You'll see this information: ```output INFO: Invalid value for parameter TimeZone "Bad" INFO: Use a name that's found exactly once in "approvedtimezonenames" ``` Do this: ```plpgsql do $body$ declare tz_out text not null := ''; begin call settimezone(makeinterval(hours=>-7)); show timezone into tz_out; assert tz_out= '<-07>+07', 'Assert <-07>+07 failed'; call settimezone(makeinterval(hours=>-5, mins=>45)); show timezone into tz_out; assert tz_out= '<-04:15>+04:15', 'Assert <-04:15>+04:15 failed'; begin call settimezone(makeinterval(hours=>19)); assert false, 'Logic error'; exception when invalidparametervalue then declare msg text not null := ''; hint text not null := ''; begin get stacked diagnostics msg = message_text, hint = pgexceptionhint; /* raise info '%', msg; raise info '%', hint; */ end; end; end; $body$; ``` The block finishes silently, showing that all of the assertions hold. Uncomment the raise info statements and repeat the test. You'll see this information: ```output INFO: Invalid value for interval "19:00:00" INFO: Use a value between "-12:00:00" and "14:00:00" ``` Do this: ```plpgsql do $body$ declare t_text constant text not null := '2021-05-31 12:00:00'; tplain constant timestamp not null := ttext; tzresult timestamptz not null := ttext||' UTC'; -- Satisfy the constraints. tzexpected timestamptz not null := ttext||' UTC'; -- The values will be overwritten tz text not null := ''; good_zones constant text[] := array[ 'UTC', 'Asia/Kathmandu', 'Europe/Amsterdam']; begin foreach tz in array good_zones loop tzresult := attimezone(tz, t_plain); tzexpected := ttext||' '||tz; declare msg constant text not null := tz||' assert failed'; begin assert tzresult = tzexpected, msg; end; end loop; end; $body$; ``` The block finishes silently, showing that all of the assertions hold. There's no value in including a bad-value negative test because doing so would simply be a repeat, and therefore redundant, test of the procedure. Do this: ```plpgsql do $body$ declare t_text constant text not null := '2021-05-31 12:00:00'; tplain constant timestamp not null := ttext; tzresult timestamptz not null := ttext||' UTC'; -- Satisfy the constraints. tzexpected timestamptz not null := ttext||' UTC'; -- The values will be overwritten i interval not null := make_interval(); hh text not null := 0; mm text not null := 0; i_vals constant interval[] not null := array[ make_interval(), make_interval(hours=>4, mins=>30), make_interval(hours=>-7) ]; begin foreach i in array i_vals loop hh := ltrim(to_char(extract(hour from i), 'SG09')); -- Essential to prefix with the sign. mm := ltrim(to_char(extract(minute from i), '09')); tzresult := attimezone(i, t_plain); tzexpected := ttext||' '||hh||':'||mm; declare msg constant text not null := i::text||' assert failed'; begin assert tzresult = tzexpected, msg; end; end loop; end; $body$; ``` The block finishes silently, showing that all of the assertions hold. There's no value in including a bad-value negative test because doing so would simply be a repeat, and therefore redundant, test of the procedure.