Add number identifier for new users in Supabase - postgresql

I want to add a trigger in a Supabase database that adds a unique identifier for new users (upon sign-up) that is shorter than the UUID that is generated, one that can be used in routing. My plan is to simply do something similar to Facebook, where I just do something like joe.smith + # of "Joe Smiths" already in the database (ex. joe.smith.12).
To do this, I am creating a function trigger:
declare
total integer;
first_name text;
last_name text;
identifier text;
begin
first_name := split_part(new.raw_user_meta_data->>'full_name', ' ', 0);
last_name := split_part(new.raw_user_meta_data->>'full_name', ' ', 1);
identifier := concat(first_name, '.', last_name, '.');
total := (select count(full_name_identifier)::int from public.profiles where full_name like concat(identifier, '%'));
insert into public.profiles (id, full_name, avatar_url, full_name_identifier)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url', concat(identifier, cast(identifier as text)));
return new;
end;
Is this correct?

Related

Create user account race condition

I have the following procedure:
CREATE OR REPLACE FUNCTION create_user(p_email character varying) RETURNS integer AS
$$
DECLARE
v_user_id integer;
BEGIN
SELECT user_id FROM user WHERE email = p_email INTO v_user_id;
IF v_user_id IS NULL THEN
INSERT
INTO user (email, status)
VALUES (p_email, 'active')
RETURNING user_id INTO v_user_id;
END IF;
RETURN v_user_id;
END;
$$ LANGUAGE plpgsql;
But if there multiple parallel requests to DB, it causes race condition, the procedure gets called multiple times and since the delay is so low, all of them trying to create a user and only one of them is succeeding. Is there any good workaround for this?
Is there any good workaround for this?
Yes, create a unique index on the email column, then use insert on conflict:
...
BEGIN
INSERT INTO "user" (email, status)
VALUES (p_email, 'active')
ON CONFLICT (email) DO NOTHING
RETURNING user_id INTO v_user_id;
RETURN v_user_id;
END;
The above will however not return the user_id if the email already exists. If you need that, you will need something like this:
...
BEGIN
INSERT INTO "user" (email, status)
VALUES (p_email, 'active')
ON CONFLICT (email) DO NOTHING
RETURNING user_id INTO v_user_id;
if v_user_id is null then
-- no insert happened
select user_id
into v_user_id
from "user"
where email = p_email;
end if;
RETURN v_user_id;
END;

Postgres mistakes parameter for column

I am new to Postgres DBs, and I am trying to write a function (like a stored procedure) to all inserting a user into my database. I know I must be missing something obvious, but I keep getting an error in the code shown where Postgres says { error: column "p_organizationsid" does not exist }. This is the snippet of code that appears to be causing the problem.
CREATE OR REPLACE FUNCTION userupsertget(
p_id uuid,
p_username character varying,
p_organizationid uuid,
p_organizationsid character varying,
p_lastname character varying,
p_firstname character varying,
p_middleinitial character varying,
p_emailaddress character varying,
p_sid character varying,
p_languageid integer,
p_forcepasswordreset bit,
p_salt character varying,
p_hash character varying,
p_statusid uuid,
p_createdby uuid,
p_modifiedby uuid,
p_lastsessionuguid uuid,
p_roleids uuid[],
p_applicationid uuid)
RETURNS SETOF users
LANGUAGE 'plpgsql'
COST 100
VOLATILE
ROWS 1000
AS $BODY$
BEGIN
DO
$do$
DECLARE establishedOrgID uuid;
BEGIN
SELECT org.id FROM organization AS org
WHERE org.sid = p_organizationsid
OR org.id = p_organizationid;
IF EXISTS (SELECT 1 FROM users WHERE id = p_id) THEN
--update statement;
UPDATE users
SET username = p_username,
lastname = p_lastname,
firstname = p_firstname,
middleinitial = p_middleinitial,
emailaddress = p_emailaddress,
sid = p_sid,
languageid = p_languageid,
forcepasswordreset = p_forcepasswordreset,
statusid = p_statusid,
datemodified = current_timestamp,
createdby = p_createdby,
modifiedby = p_modifiedby,
lastsessionguid = p_lastsessionguid
WHERE id = p_id;
IF EXISTS (SELECT 1 FROM users WHERE id = p_id AND statusid <> p_statuid) THEN
UPDATE users SET statusid = p_statusid, datestatusmodified = current_timestamp WHERE id = p_id;
END IF;
ELSE
INSERT INTO users (id, organizationid, username, lastname, firstname, middleinitial,
emailaddress, sid, languageid, forcepasswordreset, statusid, datecreated,
datemodified, createdby, modifiedby, lastsessionguid)
SELECT p_id, establishedOrgID, p_username, p_lastname, p_firstname, p_middleinitial, p_emailaddress, p_sid,
p_languageid, p_forcepasswordreset, p_statusid, current_timestamp, current_timestamp,
p_createdby, p_modifiedby, p_lastsessionuguid
FROM users
WHERE id = p_id;
-- DECLARE newid uuid := LASTVAL();
INSERT INTO users (id, userid, roleid, applicationid, startdate, enddate, createdby, modifiedBy)
SELECT LASTVAL(), roleid, p_applicationid, current_timestamp, current_timestamp, p_createdby, p_modifiedby
FROM UNNEST (p_roleids) AS roleids;
END IF;
END
$do$;
RETURN QUERY
SELECT usr.*, org.uselogin
FROM users AS usr
INNER JOIN organization AS org On org.id = usr.organizationid
WHERE id = p_id;
END;
$BODY$;
As you can see in the PostgreSQL documentation about Creating a function: https://www.postgresql.org/docs/9.1/sql-createfunction.html
you should get better luck if you replace your parameter's name by $n where n is the number of the function parameter:
CREATE FUNCTION add(integer, integer) RETURNS integer
AS 'select $1 + $2;'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
or...
CREATE FUNCTION check_password(uname TEXT, pass TEXT)
RETURNS BOOLEAN AS $$
DECLARE passed BOOLEAN;
BEGIN
SELECT (pwd = $2) INTO passed
FROM pwds
WHERE username = $1;
RETURN passed;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
-- Set a secure search_path: trusted schema(s), then 'pg_temp'.
SET search_path = admin, pg_temp;
Now, if you wish to continue using parameter names, you should declare them as alias:
CREATE or REPLACE FUNCTION data_ctl(opcao char, fdata date, fhora time) RETURNS char(10) AS $$
DECLARE
opcao ALIAS FOR $1;
vdata ALIAS FOR $2;
vhora ALIAS FOR $3;
retorno char(10);
BEGIN
IF opcao = 'I' THEN
insert into datas (data, hora) values (vdata, vhora);
retorno := 'INSERT';
END IF;
IF opcao = 'U' THEN
update datas set data = vdata, hora = vhora where data='1995-11-01';
retorno := 'UPDATE';
END IF;
IF opcao = 'D' THEN
delete from datas where data = vdata;
retorno := 'DELETE';
ELSE
retorno := 'NENHUMA';
END IF;
RETURN retorno;
END;
$$
LANGUAGE plpgsql;
--select data_ctl('I','1996-11-01', '08:15');
select data_ctl('U','1997-11-01','06:36');
select data_ctl('U','1997-11-01','06:36');

PL/pgSQL: How to use IF NEW.<variable_column_name> <> OLD.<variable_column_name>

I am pretty new to PL/pgSQL programming. I have a requirement of audit logging updated columns in my table
Table
create table sample_table(name varchar(15),city varchar(15),age int,mail varchar(20) primary key);
Audit table
create table sample_table__audits_dynamicols(mail varchar(20), columnchanged varchar(10), oldvalue varchar(10), changed_on timestamp(6) NOT NULL)
Trigger Function
CREATE FUNCTION public.log_sample_table_allchanges() RETURNS trigger AS $BODY$DECLARE
_colname text;
_tablename varchar(15) := 'sample_table';
_schema varchar(15) := 'public';
_changed_on time := now();
BEGIN
FOR _colname IN SELECT column_name FROM information_schema.Columns WHERE table_schema = _schema AND table_name = _tablename LOOP
IF NEW._colname <> OLD._colname THEN
INSERT INTO sample_table__audits_dynamicols(mail,columnchanged, oldvalue ,changed_on)
VALUES(OLD.mail,_colname,OLD.:_colname,_changed_on);
END IF;
END LOOP;
RETURN NEW;
END$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF;
Trigger
create TRIGGER log_sample_table_allchanges
BEFORE UPDATE
ON SAMPLE_TABLE
FOR EACH ROW
EXECUTE PROCEDURE log_sample_table_allchanges();
Requirement: Whenever a column value is changed i want to log it as
(mail, columnname, columnvalue, date)
E.g:
insert into sample_table (name, mail, city, age) values('kanta','mk#foo.com','hyd',23);
insert into sample_table (name, mail, city, age) values('kmk','mk#gmail.com','hyd',23);
So when i update like the following
update sample_table set age=24 where mail='mk#foo.com';
update sample_table set city='bza' where mail='mk#gmail.com'
I want audit table to record like
(mk#foo.com,age,23, timestamp)
(mk#gmail.com, city, hyd, timestamp)
Right now I am facing issue with column comparison in my Trigger function. Please help me rectifying my Trigger function to meet my requirement.
You may use EXECUTE to get the values of columns dynamically and do the comparison.
CREATE OR REPLACE FUNCTION public.log_sample_table_allchanges() RETURNS trigger AS
$BODY$
DECLARE
_colname text;
_tablename varchar(15) := 'sample_table';
_schema varchar(15) := 'public';
_changed_on timestamp := now();
_old_val text;
_new_val text;
BEGIN
FOR _colname IN SELECT column_name FROM information_schema.Columns WHERE table_schema = _schema AND table_name = _tablename
LOOP
EXECUTE 'SELECT $1.' || _colname || ', $2.' || _colname
USING OLD,NEW
INTO _old_val, _new_val; --get the old and new values for the column.
IF _new_val <> _old_val THEN
INSERT INTO sample_table__audits_dynamicols(mail,columnchanged, oldvalue ,changed_on)
VALUES(OLD.mail,_colname,_old_val,_changed_on);
END IF;
END LOOP;
RETURN NEW;
END$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF;
I'm not sure why you have defined mail as a PRIMARY KEY in the audits table, it will cause unique constraint violation if the same mail gets updated twice.

pl/pgsql CTE insert with parent child tables and array of ROWTYPE

I have two history tables. One is the parent and the second is the detail. In this case they are history tables that track changes in another table.
CREATE TABLE IF NOT EXISTS history (
id serial PRIMARY KEY,
tablename text,
row_id integer,
ts timestamp,
username text,
source text,
action varchar(10)
);
CREATE TABLE IF NOT EXISTS history_detail (
id serial PRIMARY KEY,
master_id integer NOT NULL references history(id),
colname text,
oldval text,
newval text
);
I then have function that will compare an existing row with a new row. The compare seems like a straight forward to me. The part I am struggling with is when I want to insert the differences into my history tables. During the compare I am storing the differences into an array of history_detail, of course at that time I do not know what the id or the parent table row will be. That is where I am getting hung up.
CREATE OR REPLACE FUNCTION update_prescriber(_npi integer, colnames text[]) RETURNS VOID AS $$
DECLARE
t text[];
p text[];
pos integer := 0;
ts text;
updstmt text := '';
sstmt text := '';
colname text;
_id integer;
_tstr text := '';
_dtl history_detail%ROWTYPE;
_dtls history_detail[] DEFAULT '{}';
BEGIN
-- get the master table row id.
SELECT id INTO _id FROM master WHERE npi = _npi;
-- these select all the rows' column values cast as text.
SELECT unnest_table('tempmaster', 'WHERE npi = ''' || _npi || '''') INTO t;
SELECT unnest_table('master', 'WHERE npi = ''' || _npi || '''') INTO p;
-- go through the arrays and compare values
FOREACH ts IN ARRAY t
LOOP
pos := pos + 1;
-- pos + 1 becuse the master table has the ID column
IF p[pos + 1] != ts THEN
colname := colnames[pos];
updstmt := updstmt || ', ' || colname || '=t.' || colname;
sstmt := sstmt || ',' || colname;
_dtl.colname := colname;
_dtl.oldval := p[pos + 1];
_dtl.newval := ts;
_dtls := array_append(dtls, dtl);
RAISE NOTICE 'THERE IS a difference at for COLUMN %, old: %, new: %', colname, p[pos + 1], ts;
END IF;
END LOOP;
RAISE NOTICE 'dtls length: %', array_length(dtls,1);
RAISE NOTICE 'dtls: %', dtls;
RAISE NOTICE 'done comparing: %', updstmt;
IF length(updstmt) > 0 THEN
WITH hist AS (
INSERT INTO history
(tablename, row_id, ts, username, source, action)
VALUES
('master', _id, current_timestamp, 'me', 'source', 'update')
RETURNING *
), dtls AS (
SELECT hist.id_
INSERT INTO history_detail
--
-- this is where I am having trouble
--
;
_tstr := 'UPDATE master
SET ' || substr(updstmt,2) || '
FROM (SELECT ' || substr(sstmt,2) || ' FROM tempmaster WHERE npi = ''' || _npi || ''') AS t
WHERE master.id = ' || _id || ';';
EXECUTE _tstr;
END IF;
END;
$$ LANGUAGE plpgsql;
In an ideal world I would be able to do all of this in a statement. I know I could do it in multiple statements wrapped inside another BEGIN..END. I would like to make sure that I do it in the most efficient way possible. I don't think that there is a way to get rid of the dynamic EXECUTE, but hopefully someone smarter than me can push me in the right direction.
Thanks for any help.
I was able to create a statement that would insert into the 2 history tables at once.
WITH hist AS (
INSERT INTO history
(tablename, row_id, ts, username, source, action)
VALUES
('master', _id, current_timestamp, 'me', 'source', 'update')
RETURNING id
), dtls AS (
SELECT (my_row).*
FROM unnest(_dtls) my_row
), inserts AS (
SELECT hist.id AS master_id,
dtls.colname AS colname,
dtls.newval AS newval,
dtls.oldval AS oldval
FROM dtls,hist
)
INSERT INTO history_detail
(master_id, colname, newval, oldval)
SELECT * FROM inserts
;
I'd still like to add the column update as something that isn't an EXECUTE statement, but I really don't think that is possible.

PostgresQL: Insert with key-value instead of two lists

PostgresQL allows you to INSERT with two lists, one of field names, the other of values.
INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', 9.99);
For long lists, it gets hard to figure out which list index you're on. Is there a way to insert by specifying the column name alongside the value, ie key-value pairs? Note: This is different than hstore.
ie.
INSERT INTO products (product_no => 1, name => 'Cheese', price => 9.99);
It is impossible for regular DML.
As an alternative:
Use list of values to make DML shorter:
INSERT INTO products (product_no, name, price) VALUES
(1, 'Cheese', 9.99),
(2, 'Sausages', 9.99),
...;
Or create function that you can execute with parameter specifying:
create or replace function insert_product(
in product_no products.product_no%type,
in name products.name%type,
in price products.price%type) returns products.product_no%type as $$
insert into products(product_no, name, price) values (product_no, name, price) returning product_no;
$$ language sql;
select insert_product(1, 'Mashrooms', 1.99); -- Parameters by order
select insert_product(product_no := 2, name := 'Cheese', price := 9.99); -- Parameters by name
select insert_product(product_no := 3, price := 19.99, name := 'Sosages'); -- Order does mot matter
I have one created for my use. The below takes json as input and creates dynamic SQL query and executes the same.
Sample Usage
create table employee(name character varying(20), address character varying(100), basic integer);
--sample call-1
call insert_into(true, 'employee', '{
"name" : "''Ravi Kumar''",
"address" : "''#1, 2nd Cross, Bangalore''",
"basic" : 35000
}',
'');
--sample call-2
call insert_into(true, 'employee', '{
"name" : "eo.name",
"address" : "eo.address",
"basic" : "eo.basic"
}',
'
from employee_old eo
');
Proceedure
CREATE or REPLACE PROCEDURE insert_into(
debug BOOLEAN,
tableName TEXT,
jsonTxt json,
fromWhere TEXT
)
LANGUAGE plpgsql
as $$
DECLARE
field TEXT;
fieldQuery TEXT;
valueQuery TEXT;
finalQuery TEXT;
noOfRecords INT;
BEGIN
IF debug THEN
raise notice 'preparing insert query';
END IF;
fieldQuery := CONCAT('INSERT INTO ', tableName, '(', E'\n');
valueQuery := CONCAT('SELECT ', E'\n');
FOR field IN SELECT * FROM json_object_keys(jsonTxt)
LOOP
fieldQuery := CONCAT(fieldQuery, field, E',\n');
valueQuery := CONCAT(valueQuery, json_extract_path_text(jsonTxt, field), E',\n');
END LOOP;
fieldQuery := RTRIM(fieldQuery, E',\n');
fieldQuery := CONCAT(fieldQuery, ')');
valueQuery := RTRIM(valueQuery, E',\n');
finalQuery := CONCAT(fieldQuery, E'\n', valueQuery, E'\n', fromWhere, ';');
IF debug THEN
RAISE NOTICE 'query:: %', finalQuery;
END IF;
EXECUTE finalQuery;
get diagnostics noOfRecords = row_count;
RAISE NOTICE 'Inserted:: %', noOfRecords;
END
$$;