Do I need TIMESTAMP WITH TIME ZONE for opening hours? - sql

Just beforehand: This is the backend for a mobile app which is why the question about time zone arises.
I just can't get my head around this. I am having shop_times which are storing the information from what time of the day until another time of the day a shop has opened.
Since I want to be able to tell at which time a shop_offer was offered, I also store the interval that tells in what period this shop_times was or is valid. A shop_offer itself has a period that it is valid in. At the moment I am using TIMESTAMP WITH TIME ZONE for all those intervals but I can't tell if I really need that.
If the user is in London and wants to see the offers and the time at which they are available, I could simply send him the date and time defined by a shop owner. I could also display this as local time so this is where I think I actually don't need a timezone information here.
On the other hand, without the time zone information I cannot ask "in how many hours is this available" because e.g. 2016-03-22 08:00:00 is not enough to tell since the mobile user might be in a different time zone. But well, I could simply store the time zone into my shop entity. After all it's the location of the shop that determines the time zone. So in case a mobile user asks that question I can just send him the time zone of the shop and he can calculate the answer to this.
So.. should I store the period information with or without time zone information?
This is how my tables currently look like:
CREATE TABLE shop_times (
-- PRIMARY KEY
id BIGSERIAL NOT NULL,
shop_id BIGINT NOT NULL,
CONSTRAINT fk__shop_times__shop
FOREIGN KEY (shop_id)
REFERENCES shop(id),
PRIMARY KEY (id, shop_id),
-- ATTRIBUTES
valid_from_day TIMESTAMP WITH TIME ZONE NOT NULL,
valid_until_day TIMESTAMP WITH TIME ZONE NOT NULL,
time_from TIME WITHOUT TIME ZONE NOT NULL,
time_to TIME WITHOUT TIME ZONE NOT NULL,
-- CONSTRAINTS
CHECK(valid_from_day <= valid_until_day),
CHECK(time_from < time_to)
);
CREATE TABLE shop_offer_time_period (
-- PRIMARY KEY
shop_times_id BIGINT NOT NULL,
shop_offer_id BIGINT NOT NULL,
shop_id BIGINT NOT NULL,
CONSTRAINT fk__shop_offer_time_period__shop_times
FOREIGN KEY (shop_times_id, shop_id)
REFERENCES shop_times(id, shop_id),
CONSTRAINT fk__shop_offer_time_period__shop_offer
FOREIGN KEY (shop_offer_id, shop_id)
REFERENCES shop_offer(id, shop_id),
PRIMARY KEY (shop_times_id, shop_offer_id, shop_id),
-- ATTRIBUTES
valid_for_days_bitmask INT NOT NULL,
price REAL NOT NULL,
valid_from_day TIMESTAMP WITH TIME ZONE NOT NULL,
valid_until_day TIMESTAMP WITH TIME ZONE NOT NULL,
);

A few things to note:
TIMESTAMP WITH TIME ZONE doesn't store the time zone. It just means that Postgres applies the current TIMEZONE setting when (1) parsing strings (2) formatting strings (3) determining which day a time falls into, e.g. for date_trunc or extract.
Internally, a TIMESTAMP WITH or WITHOUT TIME ZONE represents an instant. It is not stored as a string. The timezone only matters when you parse the store hours from user input, or when you format them to show to a shopper. No matter what timezone you're in, it's the same instant. It's just written "5pm Eastern" or "2pm Pacific".
It sounds like you need a shops.time_zone column and a shopper.time_zone column (or whatever your tables are called). That will let you parse/format these times correctly from the perspective of the user.
You don't need any time zone information to compute "in how many hours is this available?". That's because NOW() is an instant, and the offer's start time is an instant, and no matter what time zone you're in, the difference between them is always 2 hours (or whatever).
If your app is receiving timestamps from the database in JSON, then you are probably passsing them as strings. If so, I would standardize on passing them in UTC, so the app can easily interpret them and do client-side computations like "in how many hours is this available".

Related

Timezone aware uniqueness constraint

I am working with a timesheet app, used by users from multiple timezones. I am trying to introduce a unique constraint, that only allows users to clock_in or clock_out once per day in the local timezone.
Please refer to the following table declaration:
Table "public.entries"
---------------------------------------------
Column | Type |
---------------------------------------------
id | bigint |
user_id | bigint |
entry_type | string | enum(clock_in, clock_out)
created_at | timestamp(6) without time zone |
But little lost on how to handle the timezone-aware uniqueness.
Update:
I am considering 0:00 hrs to 23:55 hrs of local time zone as day.
User's timezone is stored in the users table but can move to the entries table if it helps with constraints.
I misread the question and wrote a bad answer, so here's a new one...
I assume this is a typical client-server-db setup. You need to obtain the local time zone from the client that's clocking in/out the user; Postgres doesn't know what it is. We'll figure out the user's local date from that and store it. Then we'll have a uniqueness index on <user, local date>.
I thought there'd be fancier ways to do this by storing the timestamptz with a separate time zone col and calculating the date within the uniqueness index, but Postgres doesn't allow us to use date_trunc within an index. So we're going to denormalize just a little and make things a lot easier with this additional date col.
CREATE TABLE clock_in (
user_id bigint NOT NULL,
created_at timestamptz NOT NULL, -- stores microseconds since epoch
local_date date NOT NULL, -- stores the <year, month, day> in whatever timezone the user clocked in from
-- optional for bookkeeping purposes: time_zone text NOT NULL,
UNIQUE(user_id, local_date)
);
Take a look at the official date/time type docs for further explanation of the above. IMO you shouldn't rely on DB constraints to reject bad user input. They're more of a second line of defense meant to ensure a self-consistent database. First your server should query the last clock-in and error out if it was in the same day, and also error if there was no clock-in that day. You'll be able to yield more useful error messages that way. Then you can insert...
INSERT INTO clock_in(user_id, created_at, local_date) (
SELECT 1, now(),
(date_trunc('day', now() AT TIME ZONE 'insert_users_timezone_here'))::date
);
Usage example for a client who has indicated it's in the PST timezone:
me=# CREATE TABLE clock_in ( user_id bigint NOT NULL, created_at timestamptz NOT NULL, local_date date NOT NULL, UNIQUE(user_id, local_date) );
CREATE TABLE
me=# INSERT INTO clock_in(user_id, created_at, local_date) ( SELECT 1, now(), (date_trunc('day', now() AT TIME ZONE 'PST'))::date );
INSERT 0 1
me=# INSERT INTO clock_in(user_id, created_at, local_date) ( SELECT 1, now(), (date_trunc('day', now() AT TIME ZONE 'PST'))::date );
ERROR: duplicate key value violates unique constraint "clock_in_user_id_local_date_key"
DETAIL: Key (user_id, local_date)=(1, 2022-04-13) already exists.
me=# INSERT INTO clock_in(user_id, created_at, local_date) ( SELECT 1, now(), (date_trunc('day', now() AT TIME ZONE 'PST' + interval '10' hour))::date );
INSERT 0 1
me=#
Then you'd do the same for clock-outs.
Using timestamptz instead of timestamp is deliberate. You should almost never use timestamp, for reasons other answers describe well.
Firstly, you'll probably want to use a native datetime datatype and a range one at that, e.g. tstzrange (with timezone) / tsrange (without timezone) – they allow you to natively store a start and end time – see https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-BUILTIN
You can optionally add an exclusion constraint to ensure no two shifts overlap – see: https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-CONSTRAINT if that's all you really want to ensure, then that might be enough.
If you definitely want to ensure there's only one shift starting or ending per day, you can use a function to derive a unique index:
create unique index INDEX_NAME on TABLE_NAME (date_trunc('day', lower(column_name)))
For your example specifically:
create unique index idx_unique_shift_start_time on entries (user_id, date_trunc('day', lower(active_during)))
create unique index idx_unique_shift_end_time on entries (user_id, date_trunc('day', upper(active_during)))
These two indexes take the lower or upper bounds of the range (i.e. the start time or end time), then truncate to just the day (i.e. drop the hours, minutes, seconds etc) and then combine with the user_id to give us a unique key.

How to specify my table to insert data as time in minutes and second into a column?

I have been struggling for several hours now and I have not been able to find the solution to my problem. This is an assignment, but I am stuck in this part.
CREATE TABLE Trip
(
Trip_Id SERIAL,
Origin VARCHAR(50) NOT NULL,
Destination VARCHAR(50) NOT NULL,
Date_Time_Picked TIMESTAMP NOT NULL DEFAULT CURRENT_DATE,
Estimated_Time TIME NOT NULL DEFAULT CURRENT_TIME,
Price DECIMAL NOT NULL,
PRIMARY KEY(Trip_Id)
);
INSERT INTO Trip (Origin, Destination, Estimated_Time, Price )
VALUES ('Hialeah' ,'Miami Beach', 30:00, 40.00);
The insert statement in postgreSQL shows a error because the time format. The column Estimated_Time is supposed to store the time in minutes and seconds, but the compiler shows an error because interprets 30:00 as hours and seconds. How can I handle the input of the user to save 30:00 as 30 minutes and 0 seconds. The Trip table can be modified, obviously, the insert statement requires a conversion or cast from '30:00' to Time type, but I am lost in how to do it. Unfortunately, books do not explain how this is done. I would greatly appreciate any hint or example. Thanks in advance.
as pointed out by a_horse_with_no_name and jarlh,
Estimated_Time is the duration of the trip, so the format should be interval
CREATE TABLE trip (
trip_id SERIAL,
origin VARCHAR(50) NOT NULL,
destination VARCHAR(50) NOT NULL,
date_time_picked TIMESTAMP WITHOUT TIME ZONE DEFAULT 'now'::text::date NOT NULL,
estimated_time INTERVAL,
price NUMERIC NOT NULL,
CONSTRAINT trip_pkey PRIMARY KEY(trip_id)
)
and the insert sould be
INSERT INTO Trip (Origin, Destination, Estimated_Time, Price )
VALUES ('Hialeah' ,'Miami Beach', '00:30:00', 40.00);

Formatting the default date and time for HSQLDB

I have been trying to format the date and time in the CREATE command (to apply for cases where user does not enter any value for date/time). According to the manual, there are the TO_DATE and TO_TIMESTAMP format elements. How do I fix my SQL statement?
http://hsqldb.org/doc/guide/builtinfunctions-chapt.html#N142F5
CREATE TABLE test1(
Id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"Date In" DATE DEFAULT TO_TIMESTAMP(CURRENT_DATE, 'DD/MM/YYYY'),
"Time In" TIME DEFAULT TO_TIMESTAMP(CURRENT_TIME,'HH:MM')
);
I tried with TO_CHAR but still return an error.
https://wiki.documentfoundation.org/Faq/Base/HSQLFunctions
DATE, TIMESTAMP or TIME values do not have "a format". They are stored in a binary way and formatted when they are displayed. So you can't apply "a format" to a DATE column. Additionally, calling to_timestamp() on a value that is a date makes no sense. to_timestamp() is used to convert a string (character) value to a date (or timestamp).
Just define both columns without (wrongly) applying a conversion from a string to a timestamp:
CREATE TABLE test1(
Id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"Date In" DATE DEFAULT CURRENT_DATE,
"Time In" TIME DEFAULT CURRENT_TIME
);
Given the name of the two columns, I think it would make more sense to store that in a single timestamp column:
CREATE TABLE test1(
Id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"Date Time In" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

How to stop PostgreSQL TIME field from being cut to seconds

I have a table with TIME fields created with the script below:
CREATE TABLE interval
(
id SERIAL NOT NULL
CONSTRAINT test_pkey
PRIMARY KEY,
start_time TIME NOT NULL,
end_time TIME NOT NULL
);
When I insert a row with this script:
INSERT INTO interval (start_time, end_time) VALUES ('05:06:07.112233', '11:22:33.778899');
It finishes successfully, but milliseconds part is dropped (time fields are rounded to seconds). I'm using PostgreSQL 10. According to this https://www.postgresql.org/docs/10/static/datatype-datetime.html the resolution of TIME type is 1 microsecond.
How can I achieve that resolution and stop milliseconds part being dropped?

Postgres - range for 'time without time zone' and exclude constraint

I have the following table:
create table booking (
identifier integer not null primary key,
room uuid not null,
start_time time without time zone not null,
end_time time without time zone not null
);
I want to create an exclude constraint to enforce that there are no overlapping appointments for the same room.
I tried the following:
alter table booking add constraint overlapping_times
exclude using gist
(
cast(room as text) with =,
period(start_time, end_time) with &&)
);
This has two problems:
Casting room to text is not enough, it gives: ERROR: data type text has no default operator class for access method "gist". I know in v10 there is btree_gist, but I am using v9.5 and v9.6, so I have to manually cast the uuid to a text afaik.
period(...) is wrong, but I have no idea how to construct a range of time without time zone type.
After installing btree_gist, you can do the following:
create type timerange as range (subtype = time);
alter table booking add constraint overlapping_times
exclude using gist
(
(room::text) with =,
timerange(start_time, end_time) with &&
);
If you want an expression in the constraint you need to put that into parentheses. So either (room::text) or (cast(room as text))