Running into an issue where now() behaves different than 'now' when used in a function in Postgres.
drop table if exists test_date_bug;
CREATE TABLE test_date_bug
(
id serial NOT NULL,
date1 timestamp with time zone NOT NULL DEFAULT current_timestamp,
date2 timestamp with time zone NOT NULL DEFAULT 'infinity'
)
WITH (
OIDS=FALSE
);
drop function if exists test_date_bug_function(id_param bigint);
CREATE OR REPLACE FUNCTION test_date_bug_function(id_param bigint)
RETURNS void AS
$$
BEGIN
UPDATE test_date_bug SET date2 = 'now' WHERE id = id_param;
END;
$$
LANGUAGE 'plpgsql' VOLATILE
SECURITY DEFINER
SET search_path = public, pg_temp;
insert into test_date_bug DEFAULT VALUES;
insert into test_date_bug DEFAULT VALUES;
insert into test_date_bug DEFAULT VALUES;
select 1 from test_date_bug_function(1);
wait a couple seconds
select 1 from test_date_bug_function(2);
Results:
select * from test_date_bug;
id | date1 | date2
----+-------------------------------+-------------------------------
3 | 2015-12-10 12:42:01.931554-06 | infinity
1 | 2015-12-10 12:42:01.334465-06 | 2015-12-10 12:42:09.491183-06
2 | 2015-12-10 12:42:01.335665-06 | 2015-12-10 12:42:09.491183-06
(3 rows)
I would not expect the date2 on row 2 to be the same date2 as row 1.
Replacing
UPDATE test_date_bug SET date2 = 'now' WHERE id = id_param;
With
UPDATE test_date_bug SET date2 = now() WHERE id = id_param;
Sets new date as I would expect:
select * from test_date_bug;
id | date1 | date2
----+-------------------------------+-------------------------------
3 | 2015-12-10 12:43:29.480242-06 | infinity
1 | 2015-12-10 12:43:28.451195-06 | 2015-12-10 12:43:38.496625-06
2 | 2015-12-10 12:43:28.451786-06 | 2015-12-10 12:43:43.447715-06
Thoughts ?
It's not a bug, it's a feature... There are two points here.
Substitution of 'now'
Let's have a look into the documentation (Date/Time Functions and Operators):
All the date/time data types also accept the special literal value now
to specify the current date and time (again, interpreted as the
transaction start time). Thus, the following three all return the same
result:
SELECT CURRENT_TIMESTAMP;
SELECT now();
SELECT TIMESTAMP 'now'; -- incorrect for use with DEFAULT
Tip: You do not want to use the third form when specifying a DEFAULT clause while creating a table. The system will convert now to
a timestamp as soon as the constant is parsed, so that when the
default value is needed, the time of the table creation would be used!
The first two forms will not be evaluated until the default value is
used, because they are function calls. Thus they will give the desired
behavior of defaulting to the time of row insertion.
So 'now' is converted to a timestamp at parse time.
Prepared statements
Okay, but what does it mean in regard to functions? It's easy to demonstrate that a function is interpreted each time you call it:
t=# create function test() returns timestamp as $$
begin
return 'now';
end;
$$ language plpgsql;
CREATE FUNCTION
t=# select test();
test
----------------------------
2015-12-11 11:14:43.479809
(1 row)
t=# select test();
test
----------------------------
2015-12-11 11:14:47.350266
(1 row)
In this example 'now' behaves as you expected.
What is the difference? Your function uses SQL statements, and test() does not. Lets look into the documentation again (PL/pgSQL Plan Caching):
As each expression and SQL command is first executed in the function,
the PL/pgSQL interpreter parses and analyzes the command to create a
prepared statement.
And here (Prepare Statement):
PREPARE creates a prepared statement. A prepared statement is a
server-side object that can be used to optimize performance. When the
PREPARE statement is executed, the specified statement is parsed,
analyzed, and rewritten. When an EXECUTE command is subsequently
issued, the prepared statement is planned and executed. This division
of labor avoids repetitive parse analysis work, while allowing the
execution plan to depend on the specific parameter values supplied.
Hence 'now' was converted to a timestamp when prepared statement was parsed. Let's demonstrate this by creating a prepared statement outside of a function:
t=# prepare s(integer) as UPDATE test_date_bug SET date2 = 'now' WHERE id = $1;
PREPARE
t=# execute s(1);
UPDATE 1
t=# execute s(2);
UPDATE 1
t=# select * from test_date_bug;
id | date1 | date2
----+-------------------------------+-------------------------------
3 | 2015-12-11 11:01:38.491656+03 | infinity
1 | 2015-12-11 11:01:37.91818+03 | 2015-12-11 11:40:44.339623+03
2 | 2015-12-11 11:01:37.931056+03 | 2015-12-11 11:40:44.339623+03
(3 rows)
That's what happend. 'now' was converted to a timestamp once (when prepared statement was parsed), and now() was called twice.
Egor has already pointed out the reason, but since you don't seem to be convinced I will discuss the issue further. The important part from the documentation is the following:
The system will convert now to a timestamp as soon as the constant is
parsed, so that when the default value is needed, the time of the
table creation would be used!
While this does not exactly apply to your case, it can somehow explain the behavior of your procedure. As stated in the documentation, PostgreSQL will convert 'now' to a timestamp as soon as it parses it. Since it is a procedure, this will happen after the first call of the procedure. For each consecutive call the value will be the same, because it had already been converted after the first parse. So what basically happens is that you execute select 1 from test_date_bug_function(1); which converts 'now' to a timestamp that is now persistent in your procedure, and then select 1 from test_date_bug_function(2); simply uses the already converted value.
If you're not convinced yet, you can try executing(simply the statements outside of the procedure):
UPDATE test_date_bug SET date2 = 'now' WHERE id = 1;
UPDATE test_date_bug SET date2 = 'now' WHERE id = 2;
to see that you will achieve the desired behavior. This is because each UPDATE is newly compiled, while the procedure is persistent which makes the converted value persistent as well.
Related
I have a database of orders and each order has a time_slot (of type TIME).
select id, time_slot from orders limit 5;
10 | 13:00:00
11 | 12:00:00
13 | 11:00:00
14 | 12:30:00
15 | 11:30:00
I want to make sure that only a certain number of orders can be placed in a time slot; for example, let's say I only want 8 orders per time slot.
I am using Supabase, so I would like to implement using RLS policies.
I've been through several iterations, but none of them have worked. Some complain of infinite recursion. My current approach is the following: I have created a view of time slot load.
create or replace view time_slot_load as
select time_slot, count(*)
from orders
group by time_slot;
select * from time_slot_load limit 5;
11:00:00 | 1
12:30:00 | 1
11:30:00 | 1
13:00:00 | 1
12:00:00 | 7
I can then create a policy that checks against this view.
ALTER POLICY "Only 8 orders per time slot"
ON public.orders
WITH CHECK (
(SELECT (load.count <= 8)
FROM time_slot_load load
WHERE (load.time_slot = orders.time_slot))
);
But this is not working.
Is there some way I can do this using constraints or RLS policies?
Any help is appreciated.
demo
table structure:
begin;
create table orders_count(
time_slot tstzrange ,
order_count integer default 0,
constraint order_ct_max1000 check(order_count <=4));
create table
orders(orderid int primary key, realtimestamp timestamptz , order_info text);
with a as(
SELECT x as begin_,x + interval '1 hour' as end_
FROM generate_series(
timestamp '2022-01-01',
timestamp '2022-01-02',
interval '60 min') t(x))
insert into orders_count(time_slot)
select tstzrange(a.begin_, a.end_,'[]') from a;
commit;
two table, one table for all the orders info, another table is track the timeslot's count, also make one constraint, within one timeslot no more than 4 orderid.
Then create trigger for delete/update/insert action on table orders.
for table orders_count, 20 years only 175200 rows (one hour one row).
main trigger function:
begin;
create or replace function f_trigger_orders_in()
returns trigger as
$$
begin
update orders_count set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
return null;
end;
$$ language plpgsql;
create or replace function f_trigger_orders_up()
returns trigger as
$$
begin
if OLD.realtimestamp <> NEW.realtimestamp THEN
update orders_count
set order_count = order_count -1
where time_slot #> OLD.realtimestamp;
update orders_count
set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
end if;
return null;
end
$$ language plpgsql;
create or replace function f_trigger_order_delaft()
returns trigger as
$$
BEGIN
update orders_count set order_count = order_count -1 where time_slot #> OLD.realtimestamp;
return null;
end;
$$
language plpgsql;
triggers action:
create trigger trigger_in_orders
AFTER INSERT ON public.orders FOR EACH ROW execute procedure f_trigger_orders_in();
create trigger trigger_up_orders
after update on public.orders for each row execute procedure f_trigger_orders_up();
create trigger trigger_del_orders
AFTER DELETE ON public.orders FOR EACH ROW execute procedure f_trigger_order_delaft();
I want to make sure that only a certain number of orders can be placed
in a time slot; for example, let's say I only want 8 orders per time
slot.
You cannot do that in PostgreSQL.
https://www.postgresql.org/docs/current/sql-createpolicy.html
check_expression:
Any SQL conditional expression (returning boolean). The conditional
expression cannot contain any aggregate or window functions. This
expression will be used in INSERT and UPDATE queries against the table
if row-level security is enabled. Only rows for which the expression
evaluates to true will be allowed. An error will be thrown if the
expression evaluates to false or null for any of the records inserted
or any of the records that result from the update. Note that the
check_expression is evaluated against the proposed new contents of the
row, not the original contents.
Why view won't work:
https://www.postgresql.org/docs/current/sql-createview.html
see Updatable Views section:
A view is automatically updatable if it satisfies all of the following
conditions:
The view's select list must not contain any aggregates, window functions or set-returning functions.
In POSTGRESQL how to get some value from a table into variable of integer type first and then use it latter.?
Actually I first want to get value in a variable of type integer and then use it in latter select query.
I know all other solution i.e. using join , using ;with clause but I not want all that. I only want what exactly I said.
Any working example will be good. I want all that working in a pgsql
CREATE OR REPLACE FUNCTION public."GetMedLastAdmnstrationDateTime"(_medicationid integer, _alfid integer)
RETURNS TABLE("MedLastAdminDateTime" timestamp without time zone)
LANGUAGE plpgsql
AS $function$
begin
declare _partitiondate timestamp = (select date from table_abc limit 1)
Return Query
select
e."MedDateTime" as "MedLastAdminDateTime"
from public."eMAR" e
where
e."AlfId" = _alfid
and e."MedicationId" = _medicationid
and e."MedDateTime" > _partitiondate::date
order by e."MedDateTime" desc
limit 1
;
END
$function$
;
I have a huge table with 25 million records, and a system that has a scheduled task that can execute a query. The query needs to quickly pick up latest records by create date(timestamp) column and apply some calculations. The problem with this is that the date is also kept in a table and with each execution it is updated to the latest execution date. It does work but it is very slow:
select * from request_history
where createdate > (select startdate from request_history_config)
limit 10;
it takes about 20 seconds to complete, which is rediculously slow compared to this:
set custom.startDate = '2019-06-13T18:02:04';
select * from request_history
where createdate > current_setting('custom.startDate')::timestamp
limit 10;
and this query finishes well within 100 miliseconds. The problem with this is that I can't update and save the date for the next execution! I was looking for SET variable TO statement that would allow me to grab some value from a table but all these attempts are not working:
select set_config('custom.startDate', startDate, false) from request_history_config;
// ERROR: function set_config(unknown, timestamp without time zone, boolean) does not exist
set custom.startDate to (select startDate from request_history_config);
// ERROR: syntax error at or near "("
You can do this using function like this:
CREATE OR REPLACE FUNCTION get_request_history()
RETURNS TABLE(createdate timestamp)
LANGUAGE plpgsql
AS $function$
DECLARE
start_date timestamp;
BEGIN
SELECT startdate INTO start_date FROM request_history_config;
RETURN QUERY
SELECT *
FROM request_history h
WHERE h.createdate > start_date
LIMIT 10;
END
$function$
And then use the function to get values:
select * from get_request_history()
I have a bunch of insert statements with varying number of columns for their respective tables, i need to execute these queries if the record doesn't already exist.I have tried to implement this as
do $$
begin
IF not exists (SELECT 1 FROM gst_type_customer WHERE name = 'Unregistered') THEN
insert into gst_type_customer(create_date,write_date,create_uid,write_uid,name) values((now() at time zone 'UTC'),(now() at time zone 'UTC'),1,1,'Unregistered');
END IF;
end
$$
even though the above code does work although implementing for a bulk of queries would require a lot of time, so i thought of making a stored procedure which i could call as
merge_check(insertquery,name[column to check for duplication],value)
but I'm unable to execute the insert query directly.
so far I have come up with
CREATE OR REPLACE FUNCTION merge_tabla(data text)
RETURNS void AS
$BODY$
BEGIN
execute(data);
END;
$BODY$
LANGUAGE plpgsql
select merge_table("insert into gst_type_customer(name) values('Unregistered')")
but I get an error saying
column "insert into gst_type_customer(name) values('Unregistered')" does not exist
The error You are getting is caused by using double quotes when calling the function. This should work:
select merge_table(E'insert into gst_type_customer(name) values(\'Unregistered\')'::text)
You need to use single quotes (double quotes are used for column names, single quotes for string literals) and escape any single quotes in the original query string.
You can use INSERT ... SELECT like this:
INSERT INTO gst_type_customer(create_date, write_date, create_uid, write_uid, name)
SELECT (now() at time zone 'UTC'), (now() at time zone 'UTC'), 1, 1, 'Unregistered'
WHERE NOT EXISTS (
SELECT *
FROM gst_type_customer
WHERE name = 'Unregistered'
)
postgres 9.1 supports merge command
https://www.postgresql.org/message-id/attachment/23520/sql-merge.html
sample:
MERGE CustomerAccount CA
USING (SELECT CustomerId, Sum(TransactionValue) As TransactionSum
FROM Transactions
WHERE TransactionId > 35345678
GROUP BY CustomerId) AS T
ON T.CustomerId = CA.CustomerId
WHEN MATCHED
UPDATE SET Balance = Balance - TransactionSum
WHEN NOT MATCHED
INSERT (CustomerId, Balance)
VALUES (T.CustomerId, T.TransactionSum)
;
I know how to convert a text to timestamp in postgreSQL using
SELECT to_timestamp('05 Dec 2000', 'DD Mon YYYY')
but how can I convert a text variable (inside a function) to timestamp??
In my table (table_ebscb_spa_log04) "time" is a character varying column, in which I have placed a formated date time (15-11-30 11:59:59.999 PM).
I have tried this function, in order to convert put the date time text into a variable (it always change) and convert it into timestamp...
CREATE OR REPLACE FUNCTION timediff()
RETURNS trigger AS
$BODY$
DECLARE
timeascharvar character varying;
timeastistamp timestamp;
BEGIN
IF NEW.time_type = 'Lap' THEN
SELECT t.time FROM table_ebscb_spa_log04 t INTO timeascharvar;
SELECT to_timestamp('timeascharvar', 'yy-mm-dd HH24:MI:SS.MS') INTO timeastistamp;
END IF;
RETURN timeastistamp;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION timediff()
OWNER TO postgres;
but whenever I run it in the table, it shows this ERROR message...
It seems that "to_timestamp" waits for a number to be the year, how can I get it to recognize the variable as if it were numbers?
The first parameter to to_timestamp should be your var not a string containing the name of your var:
to_timestamp(timeascharvar, 'yy-mm-dd HH24:MI:SS.MS')