1

I have this function:

CREATE OR REPLACE FUNCTION laborable_day(dtDateTime TIMESTAMP WITH TIME ZONE) 
RETURNS TIMESTAMP WITH TIME ZONE AS $$
DECLARE
  _isHoliday BOOLEAN;
BEGIN
  LOOP
    SELECT COUNT(*) > 0 INTO _isHoliday 
    FROM holidays 
    WHERE holiday = DATE(dtDateTime);
    
    IF _isHoliday THEN
      dtDateTime := dtDateTime + INTERVAL '1 DAY';
    ELSE
      EXIT;
    END IF;
  END LOOP;
  RETURN dtDateTime;
END;
$$ LANGUAGE plpgsql STRICT SECURITY DEFINER;

If I run this command:

select laborable_day('2022-01-01 18:53:11.14297-05'::TIMESTAMPTZ);

I get:

+------------------------------+
| laborable_day                |
|------------------------------|
| 2022-01-02 23:53:11.14297+00 |
+------------------------------+
SELECT 1
Time: 0.012s

Why is the time zone info lost?

1 Answer 1

4

The data type timestamp with time zone (timestamptz) does not store any time zone information. That's a common misconception, inspired by the misleading name. (Blame the SQL standards committee!) You are not the first to fall for this:

The time offset is just an input modifier / output decorator. Postgres always stores UTC time internally. Basics here:

Consider this demo:

test=> SET timezone = 'UTC';
SET
test=> SELECT timestamptz '2022-01-01 18:53:11.14297-05';
         timestamptz          
------------------------------
 2022-01-01 23:53:11.14297+00
(1 row)

test=> SET timezone = 'America/New_York';
SET

test=> SELECT timestamptz '2022-01-01 18:53:11.14297-05';
         timestamptz          
------------------------------
 2022-01-01 18:53:11.14297-05

test=> SELECT timestamptz '2022-03-21 18:53:11.14297-05';
         timestamptz          
------------------------------
 2022-03-21 19:53:11.14297-04
(1 row)

Note also how DST changes for the last call. See:

This also goes to show why your current function is inherently unreliable. A plain cast from timestamptz to date assumes the current time zone setting of your session. A date is not clearly defined without also giving the timezone it applies to. If you are fine with the fuzzy definition, consider this improved (but still naive) function:

CREATE OR REPLACE FUNCTION laborable_day(INOUT _dt timestamptz)
  LANGUAGE plpgsql STRICT AS  -- why SECURITY DEFINER ?
$func$
BEGIN
   LOOP
      IF EXISTS (
         SELECT FROM holidays
         WHERE  holiday = _dt::date  -- depends on current time zone! 
         ) THEN
         _dt := _dt + interval '1 day';
      ELSE
         EXIT;
      END IF;
  END LOOP;
END
$func$;

I removed SECURITY DEFINER. Only use this when necessary, as it's potentially dangerous. Instead, grant SELECT for your holidays table to PUBLIC.

Using an INOUT parameter for simplicity.

About EXISTS:

I also replaced your mixed-case identifiers. See:

Deterministic function

To get deterministic results, also define the time zone. Like

CREATE OR REPLACE FUNCTION laborable_day(INOUT _dt timestamptz, _tz text DEFAULT 'UTC')  -- default UTC?
  LANGUAGE plpgsql STRICT AS  -- why SECURITY DEFINER ?
$func$
DECLARE
   _day date := (_dt AT TIME ZONE _tz)::date;
   _inc int := 0;
BEGIN
   LOOP
      IF EXISTS (
         SELECT FROM holidays
         WHERE  holiday = _day + _inc
         ) THEN
         _inc := _inc + 1;
      ELSE
         EXIT;
      END IF;
  END LOOP;
  
  _dt := _dt + interval '1 day' * _inc;
END
$func$;

I added 'UTC' as DEFAULT for the second parameter. Adapt to your needs. About parameter defaults:

You can just add integer to a date.

I'd suggest not to "overload" the function (create variants with different function parameters), that can get tricky.

This way you can still call the function giving just a timestamptz:

SELECT laborable_day('2022-01-01 18:53:11.14297-05');

To get results for a given timezone, i.e. 'Europe/Vienna':

SELECT laborable_day('2022-01-01 18:53:11.14297-05', 'Europe/Vienna');

Use time zone names, not abbreviations or numerical offsets, to make it work properly with DST and other oddities.

Find available time zone names in pg_catalog.pg_timezone_names.

2
  • First off, very thanks for your feedback. I'm using your deterministic function, and I run select cyp_system.laborable_day('2022-01-02 18:53:11.14297-05', 'America/Lima'); and got 2022-01-02 23:53:11.14297+00, so I run select cyp_system.laborable_day('2022-01-02 18:53:11.14297-05', 'America/Lima') AT TIME ZONE 'America/Lima'; and I got 2022-01-02 18:53:11.14297. This should be simpler, damn SQL standards committee! i will watch them burn in hell.. XD Commented Mar 21, 2022 at 5:55
  • Well, I only blame them for the misleading type name. All the rest of the complication has other roots ... ;) Commented Mar 21, 2022 at 6:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.