PostgreSQL - Loop Over Rows to Fill NULL Values - postgresql

I have a table named players which has the following data
+------+------------+
| id | username |
|------+------------|
| 1 | mike93 |
| 2 | james_op |
| 3 | will_sniff |
+------+------------+
desired result:
+------+------------+------------+
| id | username | uniqueId |
|------+------------+------------|
| 1 | mike93 | PvS3T5 |
| 2 | james_op | PqWN7C |
| 3 | will_sniff | PHtPrW |
+------+------------+------------+
I need to create a new column called uniqueId. This value is different than the default serial numeric value. uniqueId is a unique, NOT NULL, 6 characters long text with the prefix "P".
In my migration, here's the code I have so far:
ALTER TABLE players ADD COLUMN uniqueId varchar(6) UNIQUE;
(loop comes here)
ALTER TABLE players ALTER COLUMN uniqueId SET NOT NULL;
and here's the SQL code I use to generate these unique IDs
SELECT CONCAT('P', string_agg (substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ceil (random() * 62)::integer, 1), ''))
FROM generate_series(1, 5);
So, in other words, I need to create the new column without the NOT NULL constraint, loop over every already existing row, fill the NULL value with a valid ID and eventually add the NOT NULL constraint.

In theory it should be enough to run:
update players
set unique_id = (SELECT CONCAT('P', string_agg ...))
;
However, Postgres will not re-evaluate the expression in the SELECT for every row, so this generates a unique constraint violation. One workaround is to create a function (which you might want to do anyway) that generates these fake IDs
create function generate_fake_id()
returns text
as
$$
SELECT CONCAT('P', string_agg (substr('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ceil (random() * 62)::integer, 1), ''))
FROM generate_series(1, 5)
$$
language sql
volatile;
Then you can update your table using:
update players
set unique_id = generate_fake_id()
;
Online example

Related

How do I invoke function that accepts a table type argument and returns a table?

Let's say I have this function that returns a table that shows how many letters are in a person's name:
CREATE TABLE people (name varchar);
INSERT INTO people VALUES ('jill');
INSERT INTO people VALUES ('jimmy');
CREATE OR REPLACE FUNCTION letter_count(person people) RETURNS TABLE(letter varchar, count bigint) AS $$
SELECT letter, COUNT(*) count FROM regexp_split_to_table(person.name, '') letter GROUP BY letter
$$ LANGUAGE sql STABLE;
I would like to invoke the function on just the person with name = jill, and I expect a result like this, which is returned by manually invoking the query in the function (SELECT letter, COUNT(*) count FROM regexp_split_to_table('jill', '') letter GROUP BY letter;):
| letter | count |
| ------ | ----- |
| j | 1 |
| i | 1 |
| l | 2 |
If I try this query:
SELECT letter_count(people.*) FROM people WHERE people.name='jill';
I get this as a result:
| letter_count |
| ------------ |
| (i,1) |
| (l,2) |
| (j,1) |
I've tried a number of other queries (SELECT * FROM letter_count((SELECT * FROM people WHERE name='jill')); seemed promising), but with no luck.
Here is a DB fiddle to play to reproduce what I'm seeing: https://www.db-fiddle.com/f/nBqwyGknRHJeWL5sdoFhhJ/0
You put such a table function in the FROM clause like a table.
Usually a lateral join is the best way to do that:
SELECT l.*
FROM people
CROSS JOIN LATERAL letter_count(people) AS l
WHERE people.name = 'jill';

Use array of IDs to insert records into table if it does not already exist

I have created a postgresql function that takes a comma separated list of ids as input parameter. I then convert this comma separated list into an array.
CREATE FUNCTION myFunction(csvIDs text)
RETURNS void AS $$
DECLARE ids INT[];
BEGIN
ids = string_to_array(csvIDs,',');
-- INSERT INTO tableA
END; $$
LANGUAGE PLPGSQL;
What I want to do now is to INSERT a record for each of the id's(in the array) into TABLE A if the ID does not already exist in table. The new records should have value field set to 0.
Table is created like this
CREATE TABLE TableA (
id int PRIMARY KEY,
value int
);
Is this possible to do?
You can use unnest() function to get each element of your array.
create table tableA (id int);
insert into tableA values(13);
select t.ids
from (select unnest(string_to_array('12,13,14,15', ',')::int[]) ids) t
| ids |
| --: |
| 12 |
| 13 |
| 14 |
| 15 |
Now you can check if ids value exists before insert a new row.
CREATE FUNCTION myFunction(csvIDs text)
RETURNS int AS
$myFunction$
DECLARE
r_count int;
BEGIN
insert into tableA
select t.ids
from (select unnest(string_to_array(csvIDs,',')::int[]) ids) t
where not exists (select 1 from tableA where id = t.ids);
GET DIAGNOSTICS r_count = ROW_COUNT;
return r_count;
END;
$myFunction$
LANGUAGE PLPGSQL;
select myFunction('12,13,14,15') as inserted_rows;
| inserted_rows |
| ------------: |
| 3 |
select * from tableA;
| id |
| -: |
| 13 |
| 12 |
| 14 |
| 15 |
dbfiddle here

Postgres Translate column value into schema prefix in a query

I have a database that uses postgresql schemas for multi-tenancy purposes. It has a table in the public schema called customers with an id and tenant column. The value for tenant is a string, and there's a corresponding postgresql schema with tables in it that match.
It looks like this:
# public.customers # first.users # second.users
| id | tenant | | id | name | | id | name |
|----|--------| |----|--------| |----|--------|
| 1 | first | | 1 | bob | | 1 | jen |
| 2 | second | | 2 | jess | | 2 | mike |
I'm wondering how I could make a single query to fetch values from a table in the schema, just given a customer id.
So if I have a customer_id of 1, how can I select * from first.users in a single query.
I'm guessing this might have to be a function written in pgpsql, but I don't have a lot of experience with that. Something like:
select * from tenant_table(1, 'users');
?
create or replace function f(_id int)
returns table (id int, name text) as $f$
declare _tenant text;
begin;
select tenant into _tenant
from public.customers
where id = _id;
return query execute format($e$
select *
from %I.users
$e$, _tenant);
end;
$f$ language plpgsql;
You cannot do that with a single query.
You'll have to use one query that selects the schema name, then construct a second query and run that.
Of course you can define a PL/pgSQL function that does both for you and executes the dynamic query with EXECUTE.

postgresql trigger for filling new column on insert

I have a table Table_A:
\d "Table_A";
Table "public.Table_A"
Column | Type | Modifiers
----------+---------+-------------------------------------------------------------
id | integer | not null default nextval('"Table_A_id_seq"'::regclass)
field1 | bigint |
field2 | bigint |
and now I want to add a new column. So I run:
ALTER TABLE "Table_A" ADD COLUMN "newId" BIGINT DEFAULT NULL;
now I have:
\d "Table_A";
Table "public.Table_A"
Column | Type | Modifiers
----------+---------+-------------------------------------------------------------
id | integer | not null default nextval('"Table_A_id_seq"'::regclass)
field1 | bigint |
field2 | bigint |
newId | bigint |
And I want newId to be filled with the same value as id for new/updated rows.
I created the following function and trigger:
CREATE OR REPLACE FUNCTION autoFillNewId() RETURNS TRIGGER AS $$
BEGIN
NEW."newId" := NEW."id";
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER "newIdAutoFill" AFTER INSERT OR UPDATE ON "Table_A" EXECUTE PROCEDURE autoFillNewId();
Now if I insert something with:
INSERT INTO "Table_A" values (97, 1, 97);
newId is not filled:
select * from "Table_A" where id = 97;
id | field1 | field2 | newId
----+----------+----------+-------
97 | 1 | 97 |
Note: I also tried with FOR EACH ROW from some answer here in SO
What's missing me?
You need a BEFORE INSERT OR UPDATE ... FOR EACH ROW trigger to make this work:
CREATE TRIGGER "newIdAutoFill"
BEFORE INSERT OR UPDATE ON "Table_A"
FOR EACH ROW EXECUTE PROCEDURE autoFillNewId();
A BEFORE trigger takes place before the new row is inserted or updated, so you can still makes changes to the field values. An AFTER trigger is useful to implement some side effect, like auditing of changes or cascading changes to other tables.
By default, triggers are FOR EACH STATEMENT and then the NEW parameter is not defined (because the trigger does not operate on a row). So you have to specify FOR EACH ROW.

reference to a sequence column (postgresql)

I encountered a problem when creating a foreign key referencing to a sequence, see the code example below.
But on creating the tables i recieve the following error.
"Detail: Key columns "product" and "id" are of incompatible types: integer and ownseq"
I've already tried different datatypes for the product column (like smallint, bigint) but none of them is accepted.
CREATE SEQUENCE ownseq INCREMENET BY 1 MINVALUE 100 MAXVALUE 99999;
CREATE TABLE products (
id ownseq PRIMARY KEY,
...);
CREATE TABLE basket (
basket_id SERIAL PRIMARY KEY,
product INTEGER FOREIGN KEY REFERENCES products(id));
CREATE SEQUENCE ownseq INCREMENT BY 1 MINVALUE 100 MAXVALUE 99999;
CREATE TABLE products (
id integer PRIMARY KEY default nextval('ownseq'),
...
);
alter sequence ownseq owned by products.id;
The key change is that id is defined as an integer, rather than as ownseq. This is what would happen if you used the SERIAL pseudo-type to create the sequence.
Try
CREATE TABLE products (
id INTEGER DEFAULT nextval(('ownseq'::text)::regclass) NOT NULL PRIMARY KEY,
...);
or don't create the sequence ownseq and let postgres do it for you:
CREATE TABLE products (
id SERIAL NOT NULL PRIMARY KEY
...);
In the above case the name of the sequence postgres has create should be products_id_seq.
Hope this helps.
PostgreSQL is powerful and you have just been bitten by an advanced feature.
Your DDL is quite valid but not at all what you think it is.
A sequence can be thought of as an extra-transactional simple table used for generating next values for some columns.
What you meant to do
You meant to have the id field defined thus, as per the other answer:
id integer PRIMARY KEY default nextval('ownseq'),
What you did
What you did was actually define a nested data structure for your table. Suppose I create a test sequence:
CREATE SEQUENCE testseq;
Then suppose I \d testseq on Pg 9.1, I get:
Sequence "public.testseq"
Column | Type | Value
---------------+---------+---------------------
sequence_name | name | testseq
last_value | bigint | 1
start_value | bigint | 1
increment_by | bigint | 1
max_value | bigint | 9223372036854775807
min_value | bigint | 1
cache_value | bigint | 1
log_cnt | bigint | 0
is_cycled | boolean | f
is_called | boolean | f
This is the definition of the type the sequence used.
Now suppose I:
create table seqtest (test testseq, id serial);
I can insert into it:
INSERT INTO seqtest (id, test) values (default, '("testseq",3,4,1,133445,1,1,0,f,f)');
I can then select from it:
select * from seqtest;
test | id
----------------------------------+----
(testseq,3,4,1,133445,1,1,0,f,f) | 2
Moreover I can expand test:
SELECT (test).* from seqtest;
select (test).* from seqtest;
sequence_name | last_value | start_value | increment_by | max_value | min_value
| cache_value | log_cnt | is_cycled | is_called
---------------+------------+-------------+--------------+-----------+----------
-+-------------+---------+-----------+-----------
| | | | |
| | | |
testseq | 3 | 4 | 1 | 133445 | 1
| 1 | 0 | f | f
(2 rows)
This sort of thing is actually very powerful in PostgreSQL but full of unexpected corners (for example not null and check constraints don't work as expected with nested data types). I don't generally recommend nested data types, but it is worth knowing that PostgreSQL can do this and will be happy to accept SQL commands to do it without warning.