Project: YugabyteDB (App Definition and Development / Database)
File: custom-interval-domains.md

title: Defining and using custom interval domains [YSQL] headerTitle: Custom domain types for specializing the native interval functionality linkTitle: Custom interval domains description: Explains how to define and use custom interval domain types to specialize the native interval functionality. [YSQL] menu: v2.18: identifier: custom-interval-domains parent: type-interval weight: 90 type: docs {{< tip title="Download the kit to create the custom interval domains." >}} The code that this page presents is included in a larger set of useful reusable date-time code. In particular, it also installs the . The custom interval domains code depends on some of these utilities. See the section . {{< /tip >}} Each of the sections and makes the point that hybrid interval arithmetic is dangerous and recommends that you should ensure that you create and use only "pure months", "pure seconds", or "pure days" interval values. And they recommend the adoption of the approach that this section describes so that your good practice will be ensured by using its APIs rather than the native interval functionality. The basic idea is to create a user-defined domain for each of the three kinds of "pure" interval, defining each with a constraint function that ensures the purity and checks that the mm, dd, or ss component of the internal representation lies within a meaningful range (see the section ). Domain-specific functions create a new value of the domain by each of these methods: From a parameterization that uses values, respectively: for years and months; for days; or for hours, minutes, and seconds. By subtracting one timestamptz value from another. By multiplying an existing value of the domain by a real number. The design of the code is the same for each of the three domains. The code for the second and third domains is trivially derived, using copy and massage, from the code for the first domain. Each of the functions intervalXok() (where X is one of months, days, or seconds) can raise the 23514 pre-defined error (mapped to checkviolation_ in PL/pgSQL) with one of these error texts: ```output value for domain intervalmonthst violates check constraint "intervalmonthsok". ``` or: ```output value for domain intervaldayst violates check constraint "intervaldaysok". ``` or: ```output value for domain intervalsecondst violates check constraint "intervalsecondsok". ``` The hint is function-specific and reflects the detail of how the constraint is violated. The constraint function intervalmonthsok() checks that only the mm component of the [mm, dd, ss] tuple is non-zero and that it lies within a sensible range. It uses the value 3,587,867 for the constant maxmm for the range check. Create the intervalmonthst_ domain thus: ```plpgsql drop domain if exists intervalmonthst cascade; drop function if exists intervalmonthsok(interval) cascade; drop function if exists mmvalueok(int) cascade; create function mmvalueok(mm in int) returns text language plpgsql as $body$ declare max_mm constant bigint not null := 3587867; begin return case abs(mm) > max_mm when true then 'Bad mm: '||mm::text||'. Must be in [-'||maxmm||', '||maxmm||'].' else '' end; end; $body$; create function intervalmonthsok(i in interval) returns boolean language plpgsql as $body$ begin if i is null then return true; else declare mmddss constant intervalmmddsst not null := intervalmmdd_ss(i); mm constant int not null := mmddss.mm; dd constant int not null := mmddss.dd; ss constant double precision not null := mmddss.ss; chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalmonthst violates check constraint "intervalmonthsok".'; begin if dd <> 0 or ss <> 0.0 then begin raise exception using errcode = chk_violation, message = msg, hint = case when dd <> 0 and ss <> 0.0 then 'dd = '||dd::text||'. ss =

Both must be zero' when dd <> 0 then 'dd = '||dd::text||'. Both dd and ss must be zero' when ss <> 0.0 then 'ss = '||ss::text||'. Both dd and ss must be zero' end; end; end if; declare hint constant text not null := mmvalueok(mm); begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; end; return true; end; end if; end; $body$; create domain intervalmonthst as interval constraint intervalmonthsok check(intervalmonthsok(value)); ``` Do these four basic sanity tests in order: ```plpgsql select ('1 month 1 day'::interval)::intervalmonthst; select ('1 month 0.000001 sec'::interval)::intervalmonthst; select ('1 month 1 day 0.000001 sec'::interval)::intervalmonthst; select ('3587868 month'::interval)::intervalmonthst; ``` They cause, respectively, these expected errors: ```output ERROR: 23514: value for domain intervalmonthst violates check constraint "intervalmonthsok". HINT: dd = 1. Both dd and ss must be zero ``` ```output ERROR: 23514: value for domain intervalmonthst violates check constraint "intervalmonthsok". HINT: ss = 1e-06. Both dd and ss must be zero ``` ```output ERROR: 23514: value for domain intervalmonthst violates check constraint "intervalmonthsok". HINT: dd = 1. ss = 1e-06. Both must be zero ``` ```output ERROR: 23514: value for domain intervalmonthst violates check constraint "intervalmonthsok". HINT: Bad mm: 3587868. Must be in [-3587867, 3587867]. ``` The constraint function intervaldaysok() checks that only the dd component of the [mm, dd, ss] tuple is non-zero and that it lies within a sensible range. It uses the value 109,203,489 for the constant maxdd for the range check. Create the intervaldayst_ domain thus: ```plpgsql drop domain if exists intervaldayst cascade; drop function if exists intervaldaysok(interval) cascade; drop function if exists ddvalueok(int) cascade; create function ddvalueok(dd in int) returns text language plpgsql as $body$ declare max_dd constant bigint not null := 109203489; begin return case abs(dd) > max_dd when true then 'Bad dd: '||dd::text||'. Must be in [-'||maxdd||', '||maxdd||'].' else '' end; end; $body$; create function intervaldaysok(i in interval) returns boolean language plpgsql as $body$ begin if i is null then return true; else declare mmddss constant intervalmmddsst not null := intervalmmdd_ss(i); mm constant int not null := mmddss.mm; dd constant int not null := mmddss.dd; ss constant double precision not null := mmddss.ss; chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervaldayst violates check constraint "intervaldaysok".'; begin if mm <> 0 or ss <> 0.0 then begin raise exception using errcode = chk_violation, message = msg, hint = case when mm <> 0 and ss <> 0.0 then 'mm = '||mm::text||'. ss = '||ss::text||'. Both must be zero' when mm <> 0 then 'mm = '||mm::text||'. Both mm and ss must be zero' when ss <> 0.0 then 'ss = '||ss::text||'. Both mm and ss must be zero' end; end; end if; declare hint constant text not null := ddvalueok(dd); begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; end; return true; end; end if; end; $body$; create domain intervaldayst as interval constraint intervaldaysok check(intervaldaysok(value)); ``` Do these four basic sanity tests in order: ```plpgsql select ('1 month 1 day'::interval)::intervaldayst; select ('1 day 0.000001 sec'::interval)::intervaldayst; select ('1 month 1 day 0.000001 sec'::interval)::intervaldayst; select ('109203490 day'::interval)::intervaldayst; ``` They cause, respectively, these expected errors: ```output ERROR: 23514: value for domain intervaldayst violates check constraint "intervaldaysok". HINT: mm = 1. Both mm and ss must be zero ``` ```output ERROR: 23514: value for domain intervaldayst violates check constraint "intervaldaysok". HINT: ss =

Both mm and ss must be zero ``` ```output ERROR: 23514: value for domain intervaldayst violates check constraint "intervaldaysok". HINT: mm = 1. ss = 1e-06. Both must be zero ``` ```output ERROR: 23514: value for domain intervaldayst violates check constraint "intervaldaysok". HINT: Bad dd: 109203490. Must be in [-109203489, 109203489]. ``` The constraint function intervalsecondsok() checks that only the ss component of the [mm, dd, ss] tuple is non-zero and that it lies within a sensible range. It uses the value 7,730,941,132,799 for the constant maxss for the range check. Create the intervalsecondst_ domain thus: ```plpgsql drop domain if exists intervalsecondst cascade; drop function if exists intervalsecondsok(interval) cascade; drop function if exists ssvalueok(double precision) cascade; create function ssvalueok(ss in double precision) returns text language plpgsql as $body$ declare max_ss constant double precision not null := 7730941132799.0; begin return case abs(ss) > max_ss when true then 'Bad ss: '||ss::text||'. Must be in [-'||maxss||', '||maxss||'].' else '' end; end; $body$; create function intervalsecondsok(i in interval) returns boolean language plpgsql as $body$ begin if i is null then return true; else declare mmddss constant intervalmmddsst not null := intervalmmdd_ss(i); mm constant int not null := mmddss.mm; dd constant int not null := mmddss.dd; ss constant double precision not null := mmddss.ss; chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalsecondst violates check constraint "intervalsecondsok".'; begin if mm <> 0 or dd <> 0 then begin raise exception using errcode = chk_violation, message = msg, hint = case when mm <> 0 and dd <> 0 then 'mm = '||mm::text||'. dd = '||dd::text||'. Both must be zero' when mm <> 0 then 'mm = '||mm::text||'. Both mm and dd must be zero' when dd <> 0 then 'dd = '||dd::text||'. Both mm and dd must be zero' end; end; end if; declare hint constant text not null := ssvalueok(ss); begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; end; return true; end; end if; end; $body$; create domain intervalsecondst as interval constraint intervalsecondsok check(intervalsecondsok(value)); ``` Do these four basic sanity tests in order: ```plpgsql select ('1 month 1 sec'::interval)::intervalsecondst; select ('1 day 1 sec'::interval)::intervalsecondst; select ('1 month 1 day 1 sec'::interval)::intervalsecondst; select makeinterval(secs=>7730941132799.01)::intervalseconds_t; ``` They cause, respectively, these expected errors: ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: mm = 1. Both mm and dd must be zero ``` ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: dd = 1. Both mm and dd must be zero ``` ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: mm = 1. dd = 1. Both must be zero ``` ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: Bad ss: 7730941132799.01. Must be in [-7730941132799, 7730941132799]. ``` The high-level design of the code is the same for each of the three domains. The detail is domain-specific. The variation is greatest for the function that subtracts one timestamptz value from another. {{< note title="Only 'timestamptz' overloads are presented here." >}} If you need to do interval arithmetic with values of the plain timestamp data type, then you'll need to implement overloads for that data type that correspond directly to the overloads for the timestamptz data type that are shown here. You can derive these more-or-less mechanically, with just a little

If you need to do interval arithmetic with values of the time data type, then you'll need only to implement overloads to provide functionality for the intervalsecondst domain because subtracting one time value from another cannot produce a result as long as one day and because it's meaningless to add days, months, or years to a pure time of day. Both of these exercises are left to the reader. Notice that if you follow Yugabyte's advice and persist only timestamptz values, then it's very unlikely that you will need to do interval arithmetic on values of the other moment data types. {{< /note >}} Each of the three domains, intervalmonthst, intervaldayst, and intervalsecondst, is provided with a matching set of three functions. Each of these constructs a value of the domain: either using an explicit parameterization; or by subtracting one timestamptz value from another; or by multiplying an existing domain value by a number. The intervalmonthst domain has these functions: The intervaldayst domain has these functions: The intervalsecondst domain has these functions: This function is parameterized so that you can produce only a "pure months interval value. ```plpgsql drop function if exists interval_months(int, int) cascade; create function interval_months(years in int default 0, months in int default 0) returns intervalmonthst language plpgsql as $body$ declare mm constant int not null := years*12 + months; hint constant text not null := mmvalueok(mm); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalmonthst violates check constraint "intervalmonthsok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return make_interval(years=>years, months=>months); end; $body$; ``` This function provides critically useful functionality that is simply missing in the native implementation. There is no way, by directly subtracting one timestamptz value from another, to produce a "pure months" interval value (or even a hybrid interval value whose internal mm component is non-zero). You have to write your own implementation. When a months interval is added to a starting moment, the finish moment always has the same day-number (in whatever is the finish month) as the start moment has. (If this is not possible, because the target day-number doesn't exist in the target month, then the target day-number is set to that month's biggest day-number.) And it has the same time of day. When one moment is subtracted from another, the day-number and the time of day of each moment are likely to differ. This implies that the interval result from subtracting one moment from another cannot necessarily produce the finish moment when added back to the start moment. The specification for this function therefore must depend on asserting a plausible rule. This implementation simply ignores the day number and the time of day of each of the two input moments. ```plpgsql drop function if exists interval_months(timestamptz, timestamptz) cascade; create function intervalmonths(tfinish in timestamptz, t_start in timestamptz) returns intervalmonthst language plpgsql as $body$ declare finishyear constant int not null := extract(year from tfinish); finishmonth constant int not null := extract(month from tfinish); finishADBC constant text not null := tochar(tfinish, 'BC'); finishisBC constant boolean not null := case when finishADBC = 'BC' then true when finishADBC = 'AD' then false end; startyear constant int not null := extract(year from tstart); startmonth constant int not null := extract(month from tstart); startADBC constant text not null := tochar(tstart, 'BC'); startisBC constant boolean not null := case when startADBC = 'BC' then true when startADBC = 'AD' then false end; -- There is no "year

Therefore, when the two input moments straddle -- the AD/BC boundary, we must subtract 12 months to the computed months difference diffasmonths constant int not null := ( (finishyear*12 + finishmonth) (startyear*12 + startmonth) ) case (finishisBC = startisBC) when true then 0 else 12 end; hint constant text not null := mmvalueok(diffasmonths); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalmonthst violates check constraint "intervalmonthsok".'; begin -- You can reason that "intervalmonths(largestlegaltimestamptzvalue, smallestlegaltimestamptz_value)" -- give mm = 3587867 and that because mmvalueok() tests if this value is exceded, "hint" will always be -- the empty string and that the following test is unnecessary. It's done for symmetry and completeness. if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return intervalmonths(months=>diffas_months); end; $body$; ``` Test it like this: ```plpgsql select interval_months('2020-06-07 13:47:19 UTC'::timestamptz, '2013-10-23 17:42:09 UTC'::timestamptz); ``` This is the result: ```output 6 years 8 mons ``` The logic of this function is trivial. But it's essential in order to maintain the status of the intervalmonthst value as "pure months" under multiplication or division. See the section . If you multiply a native interval value by a real number (or divide it), then it's more than likely that fractional months will spill down to days and beyond. Try this: ```plpgsq select make_interval(years=>3, months=>99)*0.5378; ``` This is the result: ```output 6 years 18 days 02:09:36 ``` Try the native `*` operator on the corresponding intervalmonthst value instead: ```plpgsql select (intervalmonths(years=>3, months=>99)*0.5378)::intervalmonths_t; ``` The attempt causes this error: ```output ERROR: 23514: value for domain intervalmonthst violates check constraint "intervalmonthsok". HINT: dd = 18. ss = 7776. Both must be zero ``` The function interval\months(interval\months\t, double precision)_ fixes this. Create it thus: ```plpgsql drop function if exists intervalmonths(intervalmonths_t, double precision) cascade; create function intervalmonths(i in intervalmonths_t, f in double precision) returns intervalmonthst language plpgsql as $body$ declare mm constant double precision not null := (intervalmmdd_ss(i)).mm; mmxf constant int not null := round(mm*f); hint constant text not null := mmvalueok(mmxf); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalmonthst violates check constraint "intervalmonthsok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return intervalmonths(months=>mmx_f); end; $body$; ``` Test it like this: ```plpgsql select intervalmonths(intervalmonths(years=>3, months=>99), 0.5378); ``` This is the result: ```output 6 years 1 mon ``` Compare this with the result (above) that the native `*` operator produces with a native interval value: ```output 6 years 18 days 02:09:36 ``` The "impure" part, 18 days 02:09:36, is more than half way through the month, so the approximation is what you'd want. More significantly, you must maintain the purity of the interval value in order to bring understandable semantics under subsequent operations like adding the value to a timestamptz value. This function is parameterized so that you can produce only a "pure days interval value. ```plpgsql drop function if exists interval_days(int) cascade; create function interval_days(days in int default 0) returns intervaldayst language plpgsql as $body$ declare hint constant text not null := ddvalueok(days); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervaldayst violates check constraint "intervaldaysok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return make_interval(days=>days); end; $body$; ``` This function provides critically useful functionality that is simply missing in the native implementation. There is no way, by directly subtracting one timestamptz value from another, to guarantee that you produce a "pure days" interval value. You have to write your own implementation. When a days interval is added to a starting moment, the finish moment always has the same time of

But when one moment is subtracted from another, the times of day of each moment are likely to differ. This implies that the interval result from subtracting one moment from another cannot necessarily produce the finish moment when added back to the start moment. The specification for this function therefore must depend on asserting a plausible rule. This implementation simply ignores the time of day of each of the two input moments. ```plpgsql drop function if exists interval_days(timestamptz, timestamptz) cascade; create function intervaldays(tfinish in timestamptz, t_start in timestamptz) returns intervaldayst language plpgsql as $body$ declare dfinish constant date not null := tfinish::date; dstart constant date not null := tstart::date; dd constant int not null := dfinish - dstart; hint constant text not null := ddvalueok(dd); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervaldayst violates check constraint "intervaldaysok".'; begin -- You can reason that "intervaldays(largestlegaltimestamptzvalue, smallestlegaltimestamptz_value)" -- give dd = 109203489 and that because ddvalueok() tests if this value is exceded, "hint" will always be -- the empty string and that the following test is unnecessary. It's done for symmetry and completeness. if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return interval_days(days=>dd); end; $body$; ``` Test it like this: ```plpgsql select interval_days('2020-06-07 13:47:19 UTC'::timestamptz, '2013-10-23 17:42:09 UTC'::timestamptz); ``` This is the result: ```output 2419 days ``` Notice that the two timestamptz actual arguments for this test are the same as those that were used for the intervalmonths(timestamptz, timestamptz) test. That produced the result 6 years 8 mons. But (6\12 + 8)\30 is 2400. The disagreement between 2419 days for the intervaldays() test and the effective 2400 for the intervalmonths() test reflects the critical difference between "days interval" arithmetic semantics and "months interval"_ arithmetic semantics. The logic of this function is trivial. But it's essential in order to maintain the status of the intervaldayst value as "pure days" under multiplication or division. See the section . If you multiply a native interval value by a real number (or divide it), then it's more than likely that fractional days will spill down to hours and beyond. Try this: ```plpgsq select make_interval(days=>99)*7.5378; ``` This is the result: ```output 746 days 05:48:46.08 ``` Try the native `*` operator on the corresponding intervaldayst value instead: ```plpgsql select (intervaldays(days=>99)*7.5378)::intervaldays_t; ``` The attempt causes this error: ```output ERROR: 23514: value for domain intervaldayst violates check constraint "intervaldaysok". HINT: ss = 20926.08. Both mm and ss must be zero ``` The function interval\days(interval\days\t, double precision)_ fixes this. Create it thus: ```plpgsql drop function if exists intervaldays(intervaldays_t, double precision) cascade; create function intervaldays(i in intervaldays_t, f in double precision) returns intervaldayst language plpgsql as $body$ declare dd constant double precision not null := (intervalmmdd_ss(i)).dd; ddxf constant int not null := round(dd*f); hint constant text not null := ddvalueok(ddxf); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervaldayst violates check constraint "intervaldaysok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return intervaldays(days=>ddx_f); end; $body$; ``` Test it like this: ```plpgsql select intervaldays(intervaldays(days=>99), 7.5378); ``` This is the result: ```output 746 days ``` Compare this with the result (above) that the native `*` operator produces with a native interval value: ```output 746 days 05:48:46.08 ``` You must accept the approximation in order to maintain the purity of the interval value. This is the only way to bring understandable semantics under subsequent operations like adding the value to a timestamptz

{{< note title="it's important to work around the delayed manifestation of the 22008 error" >}} Try this: ```plpgsql do $body$ declare i constant interval not null := make_interval(secs=>7730941132800); begin raise info 'Hello'; end; $body$; ``` Notice 7730941132800 is greater than the limit of 7730941132799 that the subsection gives. Yet it runs without error and reports "Hello". The error manifests only when you try to use the interval value i. Try this: ```plpgsql do $body$ declare i constant interval not null := make_interval(secs=>7730941132800); t text not null := ''; begin t := i::text; raise info 'i: %', t; end; $body$; ``` This causes the error "22008: interval out of range". It's reported for the assignment t := i::text. This is a different example of the effect that the "Limit for the ss field of the internal implementation" subsection describes thus: Anomalously, the evaluation of "i := makeinterval(secs=>secs)" (where secs is 9,435,181,535,999) silently succeeds. But the attempts to use it with, for example, extract(seconds from i) or i::text_ both cause the "interval out of range" error. Without working around this effect, The invocation intervalseconds(secs=>7730941132800)_ would report this confusing error: ```output ERROR: 22008: interval out of range CONTEXT: ...while casting return value to function's return type ``` {{< /note >}} The function intervalseconds(int, int, double precision) is parameterized so that you can produce only a "pure seconds" interval_ value. Create it thus: ```plpgsql drop function if exists interval_seconds(int, int, double precision) cascade; create function interval_seconds(hours in int default 0, mins in int default 0, secs in double precision default 0.0) returns intervalsecondst language plpgsql as $body$ declare ss constant double precision not null := (hours::double precision)6060 + (mins::double precision)*60 + secs; hint constant text not null := ssvalueok(ss); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalsecondst violates check constraint "intervalsecondsok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return make_interval(hours=>hours, mins=>mins, secs=>secs); end; $body$; ``` Do this basic sanity test: ```plpgsql select interval_seconds(secs=>7730941132800); ``` This is the result: ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: Bad ss: 7730941132800. Must be in [-7730941132799, 7730941132799]. ``` This function provides critically useful functionality that is simply missing in the native implementation. There is no way to guarantee that you produce a "pure seconds" interval value unless you write your own implementation. (For anything bigger than 24 hours, using the native functionality, you get a hybrid "days-seconds" interval value.) ```plpgsql drop function if exists interval_seconds(timestamptz, timestamptz) cascade; create function intervalseconds(tfinish in timestamptz, t_start in timestamptz) returns intervalsecondst language plpgsql as $body$ declare sfinish constant double precision not null := extract(epoch from tfinish); sstart constant double precision not null := extract(epoch from tstart); ss constant double precision not null := sfinish - sstart; hint constant text not null := ssvalueok(ss); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalsecondst violates check constraint "intervalsecondsok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return interval_seconds(secs=>ss); end; $body$; ``` Try this positive test: ```plpgsql with c as ( select interval_seconds( '247003-10-10 19:59:59 UTC'::timestamptz, '2020-01-01:12:00:00 UTC'::timestamptz) as i) select intervalmmdd_ss(i), i::text from c; ``` This is the result: ```output intervalmmdd_ss | i +- (0,0,7730941132799) | 2147483647:59:58.999552 ``` Notice that the result suffers from a tiny rounding error. It seems to be inconceivable that an application would need clock time semantics when the to-be-differenced timestamptz values are about two hundred and fifty millennia apartand so this rounding error won't

Try this negative test: ```plpgsql select interval_seconds( '247003-10-10 20:00:00 UTC'::timestamptz, '2020-01-01:12:00:00 UTC'::timestamptz); ``` As expected, it causes this error: ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: Bad ss: 7730941132800. Must be in [-7730941132799, 7730941132799]. ``` The logic of this function is trivial. Moreover, it isn't essential because the ss field of the internal tuple is a real number with microseconds precision and there is no "spill up" possibility from the ss field to the dd, or mm, fields. Try this: ```plpgsq select make_interval(hours=>99)*3.6297; ``` This is the result: ```output 359:20:25.08 ``` The native `*` operator on the corresponding intervalsecondst value also runs without error: ```plpgsql select (intervalseconds(hours=>99)*3.6297)::intervalseconds_t; ``` It brings the same result as when you use the native interval. The function interval\seconds(interval\seconds\t, double precision)_ is provided just in the interests of symmetry. Create it thus: ```plpgsql drop function if exists intervalseconds(intervalseconds_t, double precision) cascade; create function intervalseconds(i in intervalseconds_t, f in double precision) returns intervalsecondst language plpgsql as $body$ declare ss constant double precision not null := (intervalmmdd_ss(i)).ss; ssxf constant double precision not null := ss*f; hint constant text not null := ssvalueok(ssxf); chk_violation constant text not null := '23514'; msg constant text not null := 'value for domain intervalsecondst violates check constraint "intervalsecondsok".'; begin if hint <> '' then raise exception using errcode = chk_violation, message = msg, hint = hint; end if; return intervalseconds(secs=>ssx_f); end; $body$; ``` Test it like this: ```plpgsql select intervalseconds(intervalseconds(hours=>99), 3.6297); ``` Once again, It brings the same result as when you use the native interval. Now push it beyond the limit with a huge multiplier: ```plpgsql select intervalseconds(intervalseconds(hours=>99), 100000000); ``` It causes this error: ```output ERROR: 23514: value for domain intervalsecondst violates check constraint "intervalsecondsok". HINT: Bad ss: 35640000000000. Must be in [-7730941132799, 7730941132799] ``` This test uses the rules of thumb that one month is always thirty days and one day is always twenty-four

Create the table function testresults()_ thus: ```plpgsql drop function if exists test_results() cascade; create function test_results() returns table(z text) language plpgsql as $body$ declare t0 constant timestamptz not null := '2021-03-13 20:00 America/Los_Angeles'; imonths constant intervalmonthst not null := intervalmonths (months => 1); idays constant intervaldayst not null := intervaldays (days => 30); iseconds constant intervalsecondst not null := intervalseconds(hours => 30*24); t0plusimonths constant timestamptz not null := t0 + imonths; t0plusidays constant timestamptz not null := t0 + idays; t0plusiseconds constant timestamptz not null := t0 + iseconds; calculatedimonths constant intervalmonthst not null := intervalmonths (t0plusimonths, t0); calculatedidays constant intervaldayst not null := intervaldays (t0plusidays, t0); calculatediseconds constant intervalsecondst not null := intervalseconds(t0plusiseconds, t0); begin assert calculatedimonths = imonths, 'calculatedimonths <> imonths'; assert calculatedidays = idays, 'calculatedidays <> idays'; assert calculatediseconds = iseconds, 'calculatediseconds <> iseconds'; z := 't0'||rpad(' ', 40)||t0::text; return next; z := 'imonths: '||rpad(intervalmmddss(i_months)::text, 15)|| 't0 + imonths: '||t0plusimonths::text; return next; z := 'idays: '||rpad(intervalmmddss(i_days)::text, 15)|| 't0 + idays: '||t0plusidays::text; return next; z := 'iseconds: '||rpad(intervalmmddss(i_seconds)::text, 15)|| 't0 + iseconds: '||t0plusiseconds::text; return next; end; $body$; ``` Execute it using a timezone where the interval values cross the spring-forward moment: ```plpgsql set timezone = 'America/Los_Angeles'; select z from test_results(); ``` This is the result: ```output t0 2021-03-13 20:00:00-08 imonths: (1,0,0) t0 + imonths: 2021-04-13 20:00:00-07 idays: (0,30,0) t0 + idays: 2021-04-12 20:00:00-07 iseconds: (0,0,2592000) t0 + iseconds: 2021-04-12 21:00:00-07 ``` Each test result is different from the other two and is consistent, respectively, with the semantic definitions of months calendar time durations, days calendar time durations, and seconds clock time durations: The test that uses the intervalmonthst domain advances the month by one while keeping the day number the same, even though it starts from a date in March which has thirty-one days. And it keeps the local time the same even though the timezone offset has sprung forward from minus eight hours to minus seven hours. The test that uses the intervaldayst domain advances the day by thirty days. Because it starts from the thirteenth of March, which has thirty-one days, it finishes on the twelfth of April. It keeps the local time the same even though the timezone offset has sprung forward from minus eight hours to minus seven hours. The test that uses the intervalsecondst domain advances the day by thirty days to finish on the twelfth of April. It started at 20:00 local time. But because it has crossed the spring forward moment, it finishes at 21:00 local time. Create and execute the following procedure to assert that all the expected outcomes hold: ```plpgsql drop procedure if exists do_tests() cascade; create procedure do_tests() language plpgsql as $body$ declare -- Define all timestamptz values using a zero tz offset. -- Fair interpretation of "max legal value is 294276 AD" -- and "min legal value is 4713 BC". ts_max constant timestamptz not null := '294276-01-01 00:00:00 UTC AD'; ts_min constant timestamptz not null := '4713-01-01 00:00:00 UTC BC'; ts_1 constant timestamptz not null := '2021-01-01 00:00:00 UTC AD'; ts_2 constant timestamptz not null := '2000-01-01 00:00:13 UTC AD'; ts_3 constant timestamptz not null := '294275-06-01 00:00:00 UTC AD'; ts_4 constant timestamptz not null := '294275-06-01 00:00:13 UTC AD'; ts_5 constant timestamptz not null := '240271-10-10 07:59:59 UTC AD'; begin -- Do all tests using session tz 'UTC' set timezone = 'UTC'; <<"intervalmonthst tests">> begin <<"Test #1">> -- Check that given "i = tsmax - tsmin", then "tsmin + i = tsmax". declare i constant intervalmonthst not null := intervalmonths(tsmax, ts_min); tsnew constant timestamptz not null := tsmin + i; begin assert (tsnew = tsmax), 'Test #1 failure'; end "Test #1"; <<"Test #2">> -- Check that when ts2 and ts1 differ in their dd, hh, mi, or ss values, -- given "i = ts1 - ts2", then "ts2 + i <> ts1". declare i constant intervalmonthst not null := intervalmonths(ts1, ts_2); tsnew constant timestamptz not null := ts2 + i; begin assert (tsnew <> ts1), 'Test #2 failure'; end "Test #2"; end "intervalmonthst tests"; <<"intervaldayst tests">> begin <<"Test #3">> -- Check that given "i = tsmax - tsmin", then "tsmin + i = tsmax" -- for the full "tsmax, tsmin" range, declare i constant intervaldayst not null := intervaldays(tsmax, ts_min); tsnew constant timestamptz not null := tsmin + i; begin assert (tsnew = tsmax), 'Test #3 failure'; end "Test #3"; <<"Test #4">> -- Check that given "i = ts3 - tsmin", then "tsmin + i = ts3" -- where ts3 and tsmin differ by their day number but have their hh:mi:ss the same. declare i constant intervaldayst not null := intervaldays(ts3, ts_min); tsnew constant timestamptz not null := tsmin + i; begin assert (tsnew = ts3), 'Test #4 failure'; end "Test #4"; <<"Test #5">> -- Check that when ts2 and ts1 differ in their hh, mi, or ss values, -- given "i = ts4 - tsmin", then "tsmin + i <> ts4". declare i constant intervaldayst not null := intervaldays(ts4, ts_min); tsnew constant timestamptz not null := tsmin + i; begin assert (tsnew <> ts4), 'Test #5 failure'; end "Test #5"; end "intervaldayst tests"; <<"intervalsecondst tests">> begin

#6">> -- Check that given "i = ts5 - tsmin", then "tsmin + i = ts5" -- for the full "ts5, tsmin" range, declare i constant intervalsecondst not null := intervalseconds(ts5, ts_min); tsnew constant timestamptz not null := tsmin + i; ts_tol constant double precision not null := 0.0005; begin -- date_trunc('milliseconds', t) is too blunt an instrument. assert (abs(extract(epoch from tsnew) - extract(epoch from ts5)) < ts_tol), 'Test #6 failure'; end "Test #6"; end "intervalsecondst tests"; <<"Test #7">> -- Outcomes from interval multiplication/division. declare monthsresult constant intervalmonthst not null := intervalmonths (years=>6, months=>1); daysresult constant intervaldayst not null := intervaldays (days=>746); secondsresult constant intervalsecondst not null := intervalseconds(hours=>359, mins=>20, secs=>25.08); begin assert ( -- Notice the use of the "strict equals" operator. intervalmonths(intervalmonths(years=>3, months=>99), 0.5378) == months_result and intervaldays(intervaldays(days=>99), 7.5378) == days_result and intervalseconds(intervalseconds(hours=>99), 3.6297) == seconds_result ), 'Test #7 failure'; end "Test #7"; <<"Test #8">> -- Months to days ratio. declare m constant intervalmonthst not null := intervalmonths(tsmax, ts_min); mm constant double precision not null := (intervalmmdd_ss(m)).mm; ym constant double precision not null := mm/12.0; d constant intervaldayst not null := intervaldays (tsmax, ts_min); dd constant double precision not null := (intervalmmdd_ss(d)).dd; yd constant double precision not null := dd/365.2425; ratio constant double precision not null := abs(ym -yd)/greatest(ym, yd); begin assert ratio < 0.000001, 'Test #8 failure'; end "Test #8"; end; $body$; call do_tests(); ``` It finishes silently, showing that all the assertions hold. The table function secondsdaysmonthscomparison()_ creates a report thus: It uses the secs actual argument value to create the intervalsecondst value isecs_. It initializes the timestamptz value t0 to the earliest moment that PostgreSQL, and therefore YSQL, support. It initializes the timestamptz value t1 to the sum of t0 and isecs_. It initializes idays using intervaldays(t1, t0). It initializes imonths using intervalmonths(t1, t0). It evaluates intervalmmddss() for each of these interval domain values and reports the ss value that isecs represents, the dd value that idays represents, and the mm value that imonths represents. It converts each of the values ss, dd, and mm to a real number of years using these facts: the fixed number of seconds per day is 24\*60\*60 and the fixed number of months per year is 12; and the average number of days per year is 365.2425 (see the Wikipedia article ). It reports the values that it has calculated. {{< note title="365.2425 or 365.25 for the average number of days per year?" >}} The Wikipedia article gives both 365.2425 days and 365.25 days as the average number of days per year. The first figure (used in the code below) is the average according to the Gregorian scheme. And the second figure is the average according to the Julian scheme. The section presents a PL/pgSQL model for this function. This uses 365.25 days as the average number of days per year in order to produce the same result as does the native implementation that it models. (The designers of PostgreSQL might well have chosen to use 365.2425 daysbut they happened not to. The choice is arbitrary.) However, the nominal durations of the three kinds of interval in the test below are closer to each other when 365.2425 days is used. {{< /note >}} Create the table function thus: ```plpgsql drop function if exists secondsdaysmonths_comparison(double precision) cascade; create function secondsdaysmonths_comparison(secs in double precision) returns table(x text) language plpgsql as $body$ declare err text not null := ''; msg text not null := ''; hint text not null := ''; secondsperday constant double precision not null := 246060; avgdaysper_year constant double precision not null :=

avgsecondsperyear constant double precision not null := secondsperday*avgdaysperyear; monthsperyear constant double precision not null := 12; begin x := 'secs input: '||secs; return next; set timezone = 'UTC'; declare isecs constant intervalsecondst not null := intervalseconds(secs=>secs); t0 constant timestamptz not null := '4713-01-01 00:00:00 UTC BC'; t1 constant timestamptz not null := t0 + i_secs; begin declare idays constant intervaldayst not null := intervaldays (t1, t0); imonths constant intervalmonthst not null := intervalmonths(t1, t0); ss constant double precision not null := (intervalmmddss(isecs )).ss; dd constant int not null := (intervalmmddss(idays )).dd; mm constant int not null := (intervalmmddss(imonths)).mm; yrsfromss constant numeric not null := round((ss/avgsecondsper_year)::numeric, 3); yrsfromdd constant numeric not null := round((dd/avgdaysper_year )::numeric, 3); yrsfrommm constant numeric not null := round((mm/monthsperyear )::numeric, 3); begin x := 't0: '||lpad(to_char(t0, 'yyyy-mm-dd hh24:mi:ss BC'), 25); return next; x := 't1: '||lpad(to_char(t1, 'yyyy-mm-dd hh24:mi:ss BC'), 25); return next; x := ''; return next; x := 'isecs: '||isecs::text; return next; x := 'idays: '||idays::text; return next; x := 'imonths: '||imonths::text; return next; x := ''; return next; x := 'yrsfromss '||yrsfromss; return next; x := 'yrsfromdd: '||yrsfromdd; return next; x := 'yrsfrommm: '||yrsfrommm; return next; end; end; exception when check_violation then get stacked diagnostics err = returned_sqlstate, msg = message_text, hint = pgexceptionhint; x := ''; return next; x := 'ERROR: '||err; return next; x := msg; return next; x := hint; return next; end; $body$; ``` Invoke the function like this, using the biggest legal intervalsecondst value: ```plpgsql set timezone = 'UTC'; select x from secondsdaysmonths_comparison(7730941132799); ``` This is the result: ```output secs input: 7730941132799 t0: 4713-01-01 00:00:00 BC t1: 240271-10-10 07:59:58 AD i_secs: 2147483647:59:58.999552 i_days: 89478485 days i_months: 244983 years 9 mons yrsfromss 244983.772 yrsfromdd: 244983.771 yrsfrommm: 244983.750 ``` Notice that the real numbers of years calculated from each of the "pure seconds", "pure days", and "pure months" interval values are in very close agreement. The fact that they are so close, yet do differ from each other, reflects these facts: The intervalmonths() implementation uses calendar-time-semantics_ and disregards the time of day and the day number in the month. The intervaldays() implementation uses calendar-time-semantics_ and disregards the time of day. The intervalseconds() implementation uses clock-time-semantics_ and is exact to a microsecond precision. The duration of about 250 millennia is so big that the rounding errors brought by calendar-time-semantics show up only as tens of milliseconds. Now invoke the function using much smaller durations. First, like this: ```plpgsql set timezone = 'UTC'; select x from secondsdaysmonths_comparison(200000000000); ``` This is the result: ```output secs input: 200000000000 t0: 4713-01-01 00:00:00 BC t1: 1625-09-30 19:33:20 AD i_secs: 55555555:33:20 i_days: 2314814 days i_months: 6337 years 8 mons yrsfromss 6337.748 yrsfromdd: 6337.745 yrsfrommm: 6337.667 ``` And secondly like this: ```plpgsql set timezone = 'UTC'; select x from secondsdaysmonths_comparison(8854000); ``` This is the result: ```output secs input: 8854000 t0: 4713-01-01 00:00:00 BC t1: 4713-04-12 11:26:40 BC i_secs: 2459:26:40 i_days: 102 days i_months: 3 mons yrsfromss 0.281 yrsfromdd: 0.279 yrsfrommm: 0.250 ``` The input number of seconds was chosen by trial and error so that: the time of day component of t1 is about half way through the day; and the day number in the month is about half way through the month. Here, of course, the real numbers of years calculated from each of the "pure seconds", "pure days", and "pure months" interval values are in rather poor agreement. Finally, test the error behavior with an input number of seconds that exceeds the maximum value that the intervalseconds()_ constructor function allows by one second: ```plpgsql select x from secondsdaysmonths_comparison(7730941132800); ``` This is the result, as expected: ```output secs input: