Declaring intervals
This page documents the preview version (v2.23). Preview includes features under active development and is for development and testing only. For production, use the stable version (v2024.1). To learn more, see Versioning.
Download and [re]install the date-time utilities code.
The code on this page depends on the code presented in the section User-defined interval utility functions and on the function interval_mm_dd_ss (interval_parameterization_t), explained in the section Modeling the internal representation and comparing the model with the actual implementation. This is all included in the larger code kit that includes all of the reusable code that the overall date-time section describes and uses.
Even if you have already installed the code kit, install it again now. Do this because a code example in the section How does YSQL represent an interval value? redefines one of the interval utility functions.
There are over one hundred different spellings of the declaration of an interval. This might seem daunting. However, when you understand the degrees of freedom that the variations exploit, the mental model will seem straightforward. By analogy, when you consider the optional annotations of a bare numeric declaration to specify the scale and precision, you realize that multiplying the numbers of possible spellings for these two annotations gives a vast number of distinct possible spellings. But you need only to understand the concepts of scale and precision. The syntax variants for interval declarations express analogous concepts. This page explains it all. And it also points out that the variations in the syntax spellings can be grouped to reflect the fact that, within each group, the variants all express the same semantics.
However, if you follow the approach described in the section Custom domain types for specializing the native interval functionality, then you will not need to understand what this page explains.
Summary
The explanations and the supporting code below address the semantics of the different spellings of interval declarations—in other words, how these different spellings differently constrain the interval values that can be represented. The code and the explanations presented below show that though there are several syntax spellings for each, there are in fact just six kinds of interval declaration. It's convenient to call these kinds year, month, day, hour, minute, and second. The constraints are applied whenever a new interval value is created, just before recording it in the internal [mm, dd, ss] tuple format. The effects of each of the six constraints are conveniently described by this PL/pgSQL code:
-- Here with [mm, dd, ss] computed with, so far, no constraints.
case mode
when 'bare' then
null;
when 'second' then
null;
when 'minute' then
ss := trunc(ss/60.0)*60.0;
when 'hour' then
ss := trunc(ss/(60.0*60.0))*60.0*60.0;
when 'day' then
ss := 0.0;
when 'month' then
ss := 0.0;
dd := 0;
when 'year' then
ss := 0.0;
dd := 0;
mm := (mm/12)*12; -- integer division uses "trunc()" semantics
end case;
Yugabyte recommends using only the bare 'interval' declaration.
The term of art "bare interval" is to be taken literally: no trailing key words, and no (p) precision specifier.
Yugabyte staff members have carefully considered the practical value that these various constraints bring and have concluded that none captures the intent of the SQL Standard or brings useful functionality. The section Custom domain types for specializing the native interval functionality shows how you can usefully constrain interval values, declared using the bare syntax, to allow the internal interval representation to record only:
- either the years and months fields (corresponding to the mm field in the internal representation).
- or the days field (corresponding to the dd field in the internal representation).
- or the hours, minutes, and seconds fields (corresponding to the ss field in the internal representation).
If you follow this recommendation, then you don't need to study the remainder of this section.
The SQL Standard (cosmetically reworded) says this:
There are two classes of intervals. One class, called year-month intervals, has an express or implied date-time precision that includes no fields other than YEAR and MONTH, though not both are required. The other class, called day-time intervals, has an express or implied interval value precision that includes no fields other than DAY, HOUR, MINUTE, and SECOND, though not all are required.
Briefly, day-time intervals implement clock-time-semantics and year-month intervals implement calendar-time-semantics. See the section Two ways of conceiving of time: calendar-time and clock-time.
The PostgreSQL design that YSQL inherits allows, for example, the declaration spelling day to second but, as the tests below show, the day to part of the phrase has no effect and so all six fields (years, months, days, hours, minutes , and seconds) are allowed. This is not useful—in particular because the sharp distinction that the SQL Standard intends between clock-time-semantics and calendar-time-semantics is blurred: hybrid semantics emerge.
However, the regime that the custom domains approach defines is useful because the semantics of interval arithmetic for the values of each of the constrained domains is different—and honors the spirit of the SQL Standard. See the section Interval arithmetic. Moreover, the fact that there are three variants, in contrast to the SQL Standard's two variants, brings beneficial extra functionality.
Interval declaration syntax variants
The PostgreSQL documentation specifies that this is the general syntax for an interval declaration:
interval [ fields ] [ (p) ]
And it states that the fields element restricts the set of stored fields by writing one of these listed phrases (or, as the syntax diagram says, by writing nothing):
year
month
year to month
day
hour
day to hour
minute
day to minute
hour to minute
second
day to second
hour to second
minute to second
<"fields" specification omitted>
This list shows no fewer than fourteen distinct syntax spellings.
The optional (p) element specifies the precision, in microseconds, with which the seconds field of the internal representation records the value. It is legal only after the "second" keyword or after the bare interval declaration. The allowed values are in 0..6. Omitting the (p) element has the same effect as specifying (6). So with the fourteen spellings listed above, together with the eight precision designations (0, 1, 2, 3, 4, 5, 6, or omitting this element) it multiplies up to 112 distinct ways to declare an interval as a table column, in PL/pgSQL code, and so on.
Note: No pair chosen from the fourteen different interval declarations can be used to distinguish procedure or function overloads. The same applies to the variations in the specification of the (p) element.
The account below shows that the different declaration syntaxes produce only six distinct semantic outcomes. The blank lines in the list above, and in the code, reflect this by grouping the syntaxes according to their semantic outcome.
Syntax variants within each of just six groups have the same effect
The anonymous block below demonstrates the syntax groupings. It uses integral values for each of the inputs, except for the seconds, to make the results maximally understandable. The expected outcomes, used on the right hand sides of the putative equalities that the asserts test, are consistent with the explanations that the section How does YSQL represent an interval value? gave of how a parameterization that specifies values using a [yy, mm, dd, hh, mi, ss] tuple is encoded as the internal representation that uses a [mm, dd, ss] tuple with integral mm and dd fields and a real number ss field.
do $body$
declare
i_bare constant interval :=
make_interval(years=>9, months=>18, days=>700, hours=>97, mins=>86, secs=>75.123456);
i_year constant interval year := i_bare;
i_month_1 constant interval month := i_bare;
i_month_2 constant interval year to month := i_bare;
i_day constant interval day := i_bare;
i_hour_1 constant interval hour := i_bare;
i_hour_2 constant interval day to hour := i_bare;
i_minute_1 constant interval minute := i_bare;
i_minute_2 constant interval day to minute := i_bare;
i_minute_3 constant interval hour to minute := i_bare;
i_second_1 constant interval second := i_bare;
i_second_2 constant interval day to second := i_bare;
i_second_3 constant interval hour to second := i_bare;
i_second_4 constant interval minute to second := i_bare;
r_year constant text := '10 years';
r_month constant text := '10 years 6 mons';
r_day constant text := '10 years 6 mons 700 days';
r_hour constant text := '10 years 6 mons 700 days 98:00:00';
r_minute constant text := '10 years 6 mons 700 days 98:27:00';
r_second constant text := '10 years 6 mons 700 days 98:27:15.123456';
begin
-- "Year" group
assert i_year::text = r_year, 'i_year = r_year failed';
-- "Month" group
assert i_month_1::text = r_month, 'i_month_1 = r_month failed';
assert i_month_2::text = r_month, 'i_month_2 = r_month failed';
-- "Day" group
assert i_day::text = r_day, 'i_day = r_day failed';
-- "Hour" group
assert i_hour_1::text = r_hour, 'i_hour_1 = r_hour failed';
assert i_hour_2::text = r_hour, 'i_hour_2 = r_hour failed';
-- "Minute" group
assert i_minute_1::text = r_minute, 'i_minute_1 = r_minute failed';
assert i_minute_2::text = r_minute, 'i_minute_2 = r_minute failed';
assert i_minute_3::text = r_minute, 'i_minute_3 = r_minute failed';
-- "Second" group
assert i_bare ::text = r_second, 'i_bare = r_second failed';
assert i_second_1::text = r_second, 'i_second_1 = r_second failed';
assert i_second_2::text = r_second, 'i_second_2 = r_second failed';
assert i_second_3::text = r_second, 'i_second_3 = r_second failed';
assert i_second_4::text = r_second, 'i_second_4 = r_second failed';
end;
$body$;
The block finishes silently, showing that each assertion holds.
The declarations are grouped according to the six possible choices for the trailing keyword: year, month, day, hour, minute, or second. The assertions show that the syntax variants within each group have the same effect—in other words that the optional leading phrases, year to, day to, hour to, and minute to have no semantic effect. Only the trailing keyword is semantically significant. Omitting this keyword (i.e. the bare declaration) has the same semantic effect has writing second, and so it belongs in that group.
Each group has different resolution semantics: the choice of trailing keyword determines the least granular unit (years, months, days, hours, minutes, or seconds) that is respected.
The effect of the optional (p) element
Try this:
do $body$
declare
i_bare constant interval :=
'9 years 18 months 700 days 97 hours 86 minutes 75.123456 seconds';
i6 constant interval(6) not null := i_bare;
i5 constant interval(5) not null := i_bare;
i4 constant interval(4) not null := i_bare;
i3 constant interval(3) not null := i_bare;
i2 constant interval(2) not null := i_bare;
i1 constant interval(1) not null := i_bare;
i0 constant interval(0) not null := i_bare;
r6 constant text := '10 years 6 mons 700 days 98:27:15.123456';
r5 constant text := '10 years 6 mons 700 days 98:27:15.12346';
-- This shows the "round()" semantics.
r4 constant text := '10 years 6 mons 700 days 98:27:15.1235';
r3 constant text := '10 years 6 mons 700 days 98:27:15.123';
r2 constant text := '10 years 6 mons 700 days 98:27:15.12';
r1 constant text := '10 years 6 mons 700 days 98:27:15.1';
r0 constant text := '10 years 6 mons 700 days 98:27:15';
begin
-- Notice that i_bare, declared as bare "interval",
-- and i6, declared as "interval(6)" are the same.
assert i_bare ::text = r6, 'i_bare = r6 failed';
assert i6 ::text = r6, 'i6 = r6 failed';
assert i5 ::text = r5, 'i5 = r5 failed';
assert i4 ::text = r4, 'i4 = r4 failed';
assert i3 ::text = r3, 'i3 = r3 failed';
assert i2 ::text = r2, 'i2 = r2 failed';
assert i1 ::text = r1, 'i1 = r1 failed';
assert i0 ::text = r0, 'i0 = r0 failed';
end;
$body$;
The block finishes silently showing that all the assertions hold. This confirms that the (p) element determines the precision, in microseconds, with which the seconds field of the internal representation records the value. Repeat the test after globally replacing interval with interval second in the declarations. The outcome is identical.
Modeling the implementation
Make sure that you've read the section How does YSQL represent an interval value? before reading this section.
The assumption that informs the following test is that any operation that produces an interval value first computes the [mm, dd, ss] internal representation and only then applies the rules that the ad hoc test above illustrates. The rule, expressed in PL/pgSQL code, is shown in the Summary above.
The test provides new overloads for these two functions:
that each adds a mode text formal parameter to express the class name of the interval declaration as one of 'bare', 'year', 'month', 'day', 'hour', 'minute', or 'second', thus:
drop function if exists interval_mm_dd_ss(interval_parameterization_t, text) cascade;
create function interval_mm_dd_ss(p in interval_parameterization_t, mode in text)
returns interval_mm_dd_ss_t
language plpgsql
as $body$
declare
-- Use the single-parameter overload
mm_dd_ss constant interval_mm_dd_ss_t not null := interval_mm_dd_ss(p);
mm int not null := mm_dd_ss.mm;
dd int not null := mm_dd_ss.dd;
ss double precision not null := mm_dd_ss.ss;
begin
case mode
when 'bare' then
null;
when 'second' then
null;
when 'minute' then
ss := trunc(ss/60.0)*60.0;
when 'hour' then
ss := trunc(ss/(60.0*60.0))*60.0*60.0;
when 'day' then
ss := 0.0;
when 'month' then
ss := 0.0;
dd := 0;
when 'year' then
ss := 0.0;
dd := 0;
mm := (mm/12)*12; -- integer division uses "trunc()" semantics
end case;
return (mm, dd, ss)::interval_mm_dd_ss_t;
end;
$body$;
and:
drop function if exists interval_value(interval_parameterization_t, text) cascade;
create function interval_value(p in interval_parameterization_t, mode in text)
returns interval
language plpgsql
as $body$
declare
-- Use the single-parameter overload
i_bare constant interval not null := interval_value(p);
i_year constant interval year not null := i_bare;
i_month constant interval month not null := i_bare;
i_day constant interval day not null := i_bare;
i_hour constant interval hour not null := i_bare;
i_minute constant interval minute not null := i_bare;
i_second constant interval second not null := i_bare;
begin
return
case mode
when 'bare' then i_bare
when 'year' then i_year
when 'month' then i_month
when 'day' then i_day
when 'hour' then i_hour
when 'minute' then i_minute
when 'second' then i_second
end;
end;
$body$;
Test the modeled implementation of the constraints in the same way that the unconstrained model was tested in the section Modeling the internal representation and comparing the model with the actual implementation. The procedure assert_model_ok_worker() has the identical implementation to procedure assert_model_ok() in that section.
drop procedure if exists assert_model_ok_worker(interval_parameterization_t, text) cascade;
create procedure assert_model_ok_worker(p in interval_parameterization_t, mode in text)
language plpgsql
as $body$
declare
i_modeled constant interval_mm_dd_ss_t not null := interval_mm_dd_ss(p);
i_from_modeled constant interval not null := interval_value(i_modeled);
i_actual constant interval not null := interval_value(p);
mm_dd_ss_actual constant interval_mm_dd_ss_t not null := interval_mm_dd_ss(i_actual);
p_modeled constant interval_parameterization_t not null := parameterization(i_modeled);
p_actual constant interval_parameterization_t not null := parameterization(i_actual);
begin
-- Belt-and-braces check for mutual consistency among the "interval" utilities.
assert (i_modeled ~= mm_dd_ss_actual), 'assert #1 failed';
assert (p_modeled ~= p_actual ), 'assert #2 failed';
assert (i_from_modeled == i_actual ), 'assert #3 failed';
end;
$body$;
drop procedure if exists assert_model_ok(interval_parameterization_t) cascade;
create procedure assert_model_ok(p in interval_parameterization_t)
language plpgsql
as $body$
begin
call assert_model_ok_worker(p, 'bare');
call assert_model_ok_worker(p, 'year');
call assert_model_ok_worker(p, 'month');
call assert_model_ok_worker(p, 'day');
call assert_model_ok_worker(p, 'hour');
call assert_model_ok_worker(p, 'minute');
call assert_model_ok_worker(p, 'second');
end;
$body$;
Execute the tests using the same procedure test_internal_interval_representation_model() that was defined and used to test the unconstrained model. (This is also included in the code kit.)
call test_internal_interval_representation_model();
Each of the tests finishes silently showing that the rules explained above have so far been shown always to agree with the rules of the actual implementation. You are challenged to disprove the hypothesis by inventing more tests. If any of your tests causes assert_model_ok() to finish with an assert failure, then raise a GitHub issue against this documentation section.
If you do find such a counter-example, you can compare the results from using the actual implementation and the modeled implementation with this re-write of the logic of the assert_model_ok() procedure. Instead of using assert, it shows you the outputs for visual comparison. It has an almost identical implementation to the function model_vs_actual_comparison() in the section Modeling the internal representation and comparing the model with the actual implementation. The difference is that the following implentation adds the formal input parameter mode and carries this through to the new overload of interval_mm_dd_ss().
drop function if exists model_vs_actual_comparison(interval_parameterization_t, text) cascade;
create function model_vs_actual_comparison(p in interval_parameterization_t, mode in text)
returns table(x text)
language plpgsql
as $body$
declare
i_modeled constant interval_mm_dd_ss_t not null := interval_mm_dd_ss(p, mode);
i_actual constant interval not null := interval_value(p, mode);
p_modeled constant interval_parameterization_t not null := parameterization(i_modeled);
p_actual constant interval_parameterization_t not null := parameterization(i_actual);
ss_modeled_text constant text not null := ltrim(to_char(p_modeled.ss, '9999999999990.999999'));
ss_actual_text constant text not null := ltrim(to_char(p_actual.ss, '9999999999990.999999'));
begin
x := 'modeled: '||
lpad(p_modeled.yy ::text, 4)||' yy, '||
lpad(p_modeled.mm ::text, 4)||' mm, '||
lpad(p_modeled.dd ::text, 4)||' dd, '||
lpad(p_modeled.hh ::text, 4)||' hh, '||
lpad(p_modeled.mi ::text, 4)||' mi, '||
lpad(ss_modeled_text, 10)||' ss'; return next;
x := 'actual: '||
lpad(p_actual.yy ::text, 4)||' yy, '||
lpad(p_actual.mm ::text, 4)||' mm, '||
lpad(p_actual.dd ::text, 4)||' dd, '||
lpad(p_actual.hh ::text, 4)||' hh, '||
lpad(p_actual.mi ::text, 4)||' mi, '||
lpad(ss_actual_text, 10)||' ss'; return next;
end;
$body$;
Use it like this:
select x from model_vs_actual_comparison(interval_parameterization(
yy => -9.7,
mm => 1.55,
dd => -17.4,
hh => 99.7,
mi => -86.7,
ss => 75.7),
'day');
This is the result:
modeled: -9 yy, -7 mm, -1 dd, 0 hh, 0 mi, 0.000000 ss
actual: -9 yy, -7 mm, -1 dd, 0 hh, 0 mi, 0.000000 ss