Timezone aware uniqueness constraint - sql

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.

Related

CUSTOM UNIQUE CHECK POSTGRES

I am working on a task where I need to store the interviewer's time slot in the table INTERVIEW_SLOT. The table schema is like this:
CREATE TABLE INTERVIEW_SLOT (
ID SERIAL PRIMARY KEY NOT NULL,
INTERVIEWER INTEGER REFERENCES USERS(ID) NOT NULL,
START_TIME TIMESTAMP NOT NULL, -- start time of interview
END_TIME TIMESTAMP NOT NULL, -- end time of interview
IS_BOOKED BOOL NOT NULL DEFAULT 'F', -- slot is booked by any candidate or not
CREATED_ON TIMESTAMP,
-- interviewer can't give the same slot twice
CONSTRAINT UNIQUE_INTERVIEW_SLOT UNIQUE (start_time, INTERVIEWER)
);
We want to ensure that the interviewer can not give the same slot twice but the problem is with second and millisecond values of start_time. I want the UNIQUE_INTERVIEW_SLOT constant like this:
UNIQUE_INTERVIEW_SLOT UNIQUE(TO_TIMESTAMP(start_time::text, 'YYYY-MM-DD HH24:MI'), INTERVIEWER)
Is there any way to add a unique constraint that ignores the second and millisecond value?
You are looking for an exclusion constraint
create table interview_slot
(
id integer primary key generated always as identity,
interviewer integer references users(id) not null,
start_time timestamp not null, -- start time of interview
end_time timestamp not null, -- end time of interview
is_booked bool not null default 'f', -- slot is booked by any candidate or not
created_on timestamp,
constraint unique_interview_slot
exclude using gist (interviewer with =,
tsrange(date_trunc('minute', start_time), date_trunc('minute', end_time), '[]') with &&)
);
This prevents rows with overlapping start/end ranges for the same interviewer. The timestamps are "rounded" to the full minute. You need the extension btree_gist in order to create that constraint.
You can use an UNIQUE INDEX to make this check for you and truncate the timestamp to minutes:
CREATE UNIQUE INDEX idx_interview_slot_ts
ON interview_slot (interviewer, date_trunc('minutes',start_time));
Demo: db<>fiddle

Subtract two timestamptz values and insert the result into a third column

I have the following table:
CREATE TABLE duration
(
departure_time TIMESTAMPTZ,
arrival_time TIMESTAMPTZ,
duration TIME NOT NULL, -- Not sure about the datatype..
flight_id INT UNIQUE NOT NULL,
CHECK (scheduled_duration > 0),
CHECK (scheduled_arrival_time > scheduled_departure_time),
FOREIGN KEY (flight_id) REFERENCES flight(flight_id),
PRIMARY KEY (scheduled_departure_time, scheduled_arrival_time)
);
I want to calculate arrival_time - departure_time and then insert the result into the column duration. Preferably, the result of the duration subtraction would be 6h 30m. I am new to databases and PostgreSQL and I can't find a way to calculate a subtraction of two timestamps, taking into consideration their timezones at the same time.
Use a generated column
CREATE TABLE duration
(
departure_time TIME WITH TIME ZONE,
arrival_time TIME WITH TIME ZONE,
scheduled_duration INT,
flight_id INT,
duration2 TIME GENERATED ALWAYS AS ("arrival_time"::time - "departure_time"::time) STORED,
CHECK (scheduled_duration > 0),
CHECK (arrival_time > departure_time),
FOREIGN KEY (flight_id) REFERENCES flight(flight_id),
PRIMARY KEY (departure_time, arrival_time)
);
SELECT
EXTRACT(EPOCH FROM '2022-07-07 15:00:00.00000'::TIMESTAMP - '2022-07-07 15:00:00.00000'::TIMESTAMP)

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))

Do I need TIMESTAMP WITH TIME ZONE for opening hours?

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".

MySQL create time and update time timestamp

I am creating some tables where I want to store the time when a record was created and when it was last updated. I thought I could have two timestamp fields where one would have the value CURRENT_TIMESTAMP and the other would have CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP. But I guess I can't do this because you can have only 1 timestamp field with a default value in a table?
How would you recommend I get and store the two times? Thanks!
A good way to create fields like 'created' and 'updated' is
CREATE TABLE `mytable` (
`id` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
`created` TIMESTAMP DEFAULT '0000-00-00 00:00:00',
`updated` TIMESTAMP DEFAULT now() ON UPDATE now(),
`myfield` VARCHAR(255)
);
And its necessary to enter nulls into both columns during "insert":
INSERT INTO mytable (created,updated,myfield) VALUES (null,null,'blablabla');
And now, in all updates, the 'updated' field will have a new value with actual date.
UPDATE mytable SET myfield='blablablablu' WHERE myfield='blablabla';
Source : http://gusiev.com/2009/04/update-and-create-timestamps-with-mysql
As of MYSQL version 5.6.5 you can do this using DEFAULT and ON UPDATE. No triggers are needed.
ts_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ts_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
You can have two columns of type timestamp in one table.
The following works for MySQL 5.0
create table t
(
id integer,
created_at timestamp default current_timestamp,
updated_at timestamp
);
I think you are confusing this with SQL Server (where timestamp is not really a "time stamp" and there is indeed a limit on a single "timestamp" column)
Edit: But you will need a trigger to update the update_at column each time the row is changed.
As far as I know, there's no workaround for that restriction. You'll need to manually set (at least) one of the timestamps, the easiest way is just add updated = NOW() to the UPDATE-query.
You'll need two columns: CREATE_TIME and UPDATE_TIME.
You might want to add CREATE_USER and UPDATE_USER.
Perhaps you'd want to have a 1:many relationship with name of column changed, old and new values.
It's all part of change data capture. You could have CDC tables that are updated using triggers.
I would leave the current timestamp the way you suggested and fill in the created_at field with current date on insert.