postgREST route using function does not update table - postgresql

I am using Supabase which has a built in postgREST server.
In this server I have created a function that should update a value given a checksum:
CREATE OR REPLACE FUNCTION set_value(_id TEXT, _value INTEGER, _check INTEGER) RETURNS BOOLEAN AS $$
BEGIN
IF get_checksum(_value) = _check THEN
UPDATE values_table SET score = _value WHERE device_id = _id;
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;
END;
$$ LANGUAGE plpgsql;
-- the schema is reloading after creating a function according to the docs
NOTIFY pgrst, 'reload schema';
When I call this function via sql everything works cheerio and I get the TRUE response, and I can see the value is updated in the table
SELECT * FROM set_value('SOME_ID', 123, 123456)
-- | Results |
-- | true |
However, calling this via api does not seem to work. I get the correct responses (true when checksum matches, false otherwise), but the value in the table remains unchanged.
I am using the POST request below, which according to the documentation, should run in a read/write transaction.
curl --location --request POST 'https://<myapp>.supabase.co/rest/v1/rpc/set_value' \
--header 'Authorization: Bearer <mytoken>' \
--header 'apiKey: <mykey>' \
--header 'Content-Type: application/json' \
--data-raw '{
"_id": "_deviceId",
"_value": 123,
"_check": 123456
}'
What am I doing wrong?

You will need to add volatile to the function creation in order for it to perform an update on the table.
CREATE OR REPLACE FUNCTION set_value(_id TEXT, _value INTEGER, _check INTEGER) RETURNS BOOLEAN AS $$
BEGIN
IF get_checksum(_value) = _check THEN
UPDATE values_table SET score = _value WHERE device_id = _id;
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;
END;
$$ LANGUAGE plpgsql volatile;
You can read more about the volatile keyword in this other SO answer How do IMMUTABLE, STABLE and VOLATILE keywords effect behaviour of function?

Related

Updating timestamp when a field is edited in pg admin

I am modifying data directly in pg admin 4, where I have to validate manually by marking a boolean value to true that is false by default.
I want that when I modify the value in that column, the updated_at column value should also be updated to current timestamp so I can query data with modified date.
How do I achieve this?
The post desired behavior is more simple than code demo in https://www.postgresql.org/docs/current/plpgsql-trigger.html.
setup:
CREATE temp TABLE test (
misc int,
is_valid boolean,
updated_at timestamptz
);
INSERT INTO test (misc, is_valid)
VALUES (1, FALSE);
CREATE FUNCTION update_test_update_at() RETURNS trigger AS $func$
BEGIN
IF NEW.* != OLD.* THEN NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER update_last_edit
BEFORE UPDATE OF is_valid ON test
FOR EACH ROW
EXECUTE FUNCTION update_test_update_at();
test:
UPDATE
test
SET
is_valid = true
RETURNING
*;

postgresql function and trigger execute select current_setting into integer variable causes error

Postgresql 9.6.x
I am getting an error with a postgresql function where I am recording a log on every table modification. This was all working great until I added this functionality where I am recording the current user id using current_setting functionality of postgresql. I set the current user on transactions in the background like so:
select set_config('myvars.active_user_id', '2123', true)
All this functionality works perfectly fine, except when the user is not set. This occurs when the tables are being updated by back end system queries and in that case the setting 'myvars.active_user_id' is null.
I want it to be null when it is not set. The user id field in the log is nullable.
It seems to be trying to convert null to an empty string and put that in the integer variable which it doesn't like.
This appears to be some kind of weird problem specific to functions with triggers. I must be doing something wrong because as far as I know assigning a null value through a select...into"is no issue.
The error I get in that case is so:
PSQLException: ERROR: invalid input syntax for integer: ""
I have added tracing statements and it is on this line:
EXECUTE 'select current_setting(''myvars.active_user_id'', true) ' into log_user_id;
I don't understand why in this setting it gets upset about the null value. But it seems limited to this type of trigger function. Below is essentially the function I am using
CREATE OR REPLACE FUNCTION update_log() RETURNS TRIGGER AS $update_log$
DECLARE
logid int;
log_user_id int;
BEGIN
EXECUTE 'select current_setting(''myvars.active_user_id'', true) ' into log_user_id;
IF (TG_OP='DELETE') THEN
EXECUTE 'select nextval(''seq_log'') ' into logid;
-- INSERT INTO log ....
RETURN NULL;
ELSIF (TG_OP='INSERT') THEN
EXECUTE 'select nextval(''seq_log'') ' into logid;
-- INSERT INTO log ....
RETURN NEW;
ELSIF (TG_OP='UPDATE') THEN
-- INSERT INTO log ....
END IF;
END IF;
RETURN NULL;
END;
$log$ LANGUAGE plpgsql;
Any thoughts?
GUC (Global User Setting) variables like your myvars.active_user_id are not nullable internally. It holds text or empty text. These variables cannot to store NULL. So when you store NULL, then empty string is stored, and this empty string is returned from function current_setting.
In Postgres (and any database without Oracle) NULL is not empty string and empty string is not NULL.
So this error is expected:
postgres=# do $$
declare x int;
begin
perform set_config('x.xx', null, false);
execute $_$ select current_setting('x.xx', true) $_$ into x;
end;
$$;
ERROR: invalid input syntax for integer: ""
CONTEXT: PL/pgSQL function inline_code_block line 5 at EXECUTE
You need to check result first, and replace empty string by NULL:
create or replace function nullable(anyelement)
returns anyelement as $$
select case when $1 = '' then NULL else $1 end;
$$ language sql;
do $$
declare x int;
begin
perform set_config('x.xx', null, false);
execute $_$ select nullable(current_setting('x.xx', true)) $_$ into x;
end;
$$;
DO
#Laurenz Albe has big true in your comment. Use dynamic SQL (execute command) only when it is necessary. It is not this case. So your code should looks like:
do $$
declare x int;
begin
perform set_config('x.xx', null, false);
x := nullable(current_setting('x.xx', true));
end;
$$;
DO
Note: There is buildin function nullif, so your code can looks like (and sure, buildin functionality should be preferred):
do $$
declare x int;
begin
perform set_config('x.xx', null, false);
x := nullif(current_setting('x.xx', true), '');
end;
$$;
DO

PostgreSQL - computed column to return "is my" boolean

I'm new to pg and dbs in general. I found a blog db boilerplate and I'm trying to add some computed columns to return Booleans for whether the currentPerson (the authenticated user) is the author of a post. Basically, a phantom column in the posts table that has a true or false for each post as to whether the currentPerson authored it.
This function returns the currentPerson:
SELECT *
FROM myschema.person
WHERE id = nullif(current_setting('jwt.claims.person_id', true), '')::uuid
So, I would like to do something like below, but this is not correct. This appears as a mutation in graphiql (I'm using postgraphile to generate my schema).
Any tips on how to create an isMyPost boolean computed column in a posts table would be awesome.
CREATE OR REPLACE FUNCTION myschema.is_my_post(
)
RETURNS BOOLEAN AS
$func$
BEGIN
SELECT *
FROM myschema.post
WHERE author_id = nullif(current_setting('jwt.claims.person_id', true), '')::uuid;
IF FOUND THEN
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;
END
$func$ LANGUAGE plpgsql;
The answer ended up being this, thanks to #benjie #Graphile:
CREATE OR REPLACE FUNCTION myschema.post_current_person_has_reacted(
p myschema.post)
RETURNS boolean
LANGUAGE 'sql'
COST 100
STABLE PARALLEL UNSAFE
AS $BODY$
select exists(
select 1
from myschema.post_reactions pr
where pr.post_id = p.id
and pr.person_id = nullif(current_setting('jwt.claims.person_id', true), '')::uuid
)
$BODY$;

PostGreSQL - Controlling transactions

I have a Stored Procedure that in turns calls several other Stored Procedures; each of them returns true or false and internally deal with errors by store them into a table.
Something like this:
-- (MAIN STORED PROCEDURE)
BEGIN
CALL STORED_PROC_1('WW','TT','FF',result);
IF result = TRUE then
CALL STORED_PROC_2('a','b','c',result);
...
END IF;
END;
IF result != TRUE THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
-- (END MAIN STORED PROCEDURE)
-------
--Example of Stored Procedure 1
CREATE OR REPLACE PROCEDURE STORED_PROC_1 (IN a TEXT, IN b TEXT, IN c TEXT, INOUT result boolean)
AS $BODY$
BEGIN
-- DO SOME STUFF HERE
IF ERROR_FOUND THEN
INSERT INTO ERROR_LOG VALUES ('Error','Type of Error',CURRENT_DATE);
--COMMIT; (I cannot do this commit here but I would like to save the information going into the ERROR_LOG table)
result := FALSE;
ELSE
result := TRUE;
END IF;
END;
$BODY$;
This is actually what I want; only commit if all return TRUE;
The problem is that inside the STORED_PROC_1 or _2 there are error handlings that write into a Error Log table and ... if there are errors, they will return FALSE in the result and, that in turn will call a rollback and I will loose my Error Log.
Is there a way to create a sort of a memory table that I can load with the error info and write it after the ROLLBACK? Or is there a better way to achieve this?
Thanks a lot.
I would suggest using FUNCTIONs rather than PROCEDUREs if you can for all of this because they have transaction control built-in in a sense. If the FUNCTION fails you automatically rollback its trasaction.
That being said, instead of doing the insert directly I would make a function like the following:
CREATE OR REPLACE FUNCTION func_logerror(error TEXT, errortype TEXT, date DATE DEFAULT CURRENT_DATE)
RETURNS SETOF ERROR_LOG
AS $$
DECLARE
BEGIN
RETURN QUERY
INSERT INTO ERROR_LOG VALUES (error, errortype, date) RETURNING *;
END;
$$;
Call it using either SELECT or PERFORM depending on whether or not you want the results in the calling function.
PERFORM * FROM func_logerror('Error', 'Type of Error', CURRENT_DATE);
After reading your answer response I've come up with this to help clarify. I was suggesting not using stored procedures at all. Instead handle your error cases in a different manner:
-- (MAIN STORED PROCEDURE)
CREATE OR REPLACE FUNCTION FUNC_MAIN()
RETURNS boolean
AS $$
DECLARE
res boolean;
BEGIN
SELECT * INTO res FROM FUNC_1('WW','TT','FF');
IF res = TRUE
THEN
SELECT * INTO res FROM FUNC_2('a','b','c');
...
ELSE
RETURN FALSE;
END IF;
RETURN res;
END;
$$;
-- (END MAIN STORED PROCEDURE)
-------
--Example of Stored Procedure 1
CREATE OR REPLACE FUNCTION FUNC_1 (a TEXT, b TEXT, c TEXT)
RETURNS boolean
AS $$
DECLARE
BEGIN
-- ****complete data validation before writing to tables****
-- If you hit any invalid data return out of the function before writing any persistent data
-- Ex:
IF COALESCE(a, '') = '' -- could be anything that may produce ERROR_FOUND from your example
THEN
RAISE WARNING 'FUNC_1 | param: a: must have a value';
PERFORM * FROM "Schema".func_errlog('Error','Type of Error',CURRENT_DATE);
RETURN FALSE;
END IF;
RETURN TRUE; -- If you've made it to the end of the function there should be no errors
END;
$$;

Returning boolean from SQL function after INSERT command

I have a simple function for updating a column in a table. I want to return boolean to indicate if the update was successful or not.
Here is the function:
CREATE OR REPLACE FUNCTION api.file_confirm_upload_to_s3(_file_guid uuid )
RETURNS bool AS
$BODY$
UPDATE main.file
SET is_uploaded_to_s3 = true
WHERE guid = _file_guid
RETURNING CASE WHEN guid IS NULL THEN false ELSE true END
$BODY$
LANGUAGE SQL
VOLATILE
SECURITY DEFINER;
The function returns TRUE after a successful update, but it returns NULL after an unsuccessful update. Null is falsey, so it'll work for me, but I do wonder how I can solve this a bit cleaner.
An unsuccessful update returns an empty dataset, which yields NULL, hence the return value. How can I test for empty vs. non empty dataset instead of checking the value of the guid field?
Edit after JGH's answer..
Here is the code that does what I want. I had to switch to plpgsql to get access to the FOUND variable.
CREATE OR REPLACE FUNCTION api.file_confirm_upload_to_s3(_file_guid uuid )
RETURNS bool AS
$BODY$
BEGIN
UPDATE main.file
SET is_uploaded_to_s3 = true
WHERE guid = _file_guid;
RETURN FOUND;
END;
$BODY$
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER;
Just use return found;
This special variable contains a boolean indicating whether one or more row was found using the previous query.
Read its doc 41.5.5 here and example 41-2 there.
Also you can do this in SQL mode
...
with c as( -- put your update into cte
UPDATE main.file
SET is_uploaded_to_s3 = true
WHERE guid = _file_guid
RETURNING guid
)
select exists(select guid from c);
...