Creating a partial index based on declared variables inside a DO block - postgresql

I have a table on which I want to have two different partial unique indexes that include unique constraints on different columns, based on a the value of another column in the table (which is a foreign key). Here's an example:
id | col1 | col2 | col3 | col4
------------------------------
1 | 3 | 4 | 'a' | 13
2 | 2 | 2 | 'b' | 431
3 | 3 | 4 | 'b' | 18
4 | 10 | 8 | 'b' | 211
Let's say in this table I want:
to put a partial index on all the rows where col4=13 OR col4=18 with a unique constraint on col1, col2, and col3
to put a partial index on all the rows where col4<>13 AND col4<>18 with a unique constraint on col1 and col2
The problem is that I want to do this based on the value of a column in another table since col4 is a foreign key. This SO post asks a similar question but there isn't really a solution. Here is what I've done with pl/pgsql:
DO
$$
DECLARE
-- typical subquery
option1_id INTEGER := (SELECT id FROM option_table WHERE name = 'option1');
option2_id INTEGER := (SELECT id FROM option_table WHERE name = 'option2');
BEGIN
RAISE INFO '%, %', option1_id, option2_id;
-- option1
CREATE UNIQUE INDEX option1_index ON ex_table (col1, col2) WHERE (
col4 NOT IN (option1_id, option1_id)
);
-- option2
CREATE UNIQUE INDEX option2_index ON ex_table (col1, col2, col3) WHERE (
reference_type IN (option1_id, option2_id)
);
-- this works!
CREATE UNIQUE INDEX this_works ON ex_table (col1, col2, col3) WHERE (
reference_type IN (13, 18)
);
END
$$;
Here is the error that I'm getting:
ERROR: column "option1_id" does not exist
I know the variables are properly declared because the RAISE INFO is returning INFO: 13, 18

DDL statements doesn't support parametrization - you cannot to use any variables there. You should to use dynamic SQL (in this case without clause USING):
EXECUTE format('CREATE UNIQUE INDEX ... WHERE reference_type IN (%L, %L)', option1_id, option2_id);

Related

PostgreSQL UNNEST Array and Insert Into New Table

I have an array column I want to unnest, split the element values, and copy into another table. For example:
id | col1
-----------------
1 | '{"a:1", "b:2"}'
I'd like to insert into a new table that looks like:
table1_id | col1 | col2
------------------------
1 | 'a' | 1
1 | 'b' | 2
You can issue an insert from this select:
select id as table1_id,
(string_to_array(ary, ':'))[1] as col1,
(string_to_array(ary, ':'))[2] as col2
from table1
cross join lateral unnest(col1) as u(ary);
db<>fiddle here

Check constraint on biggest key of HSTORE in Postgres

I would like to create check constraint on the HSTORE field that contains data in a following format:
{
1 => 2020-03-01, 2 => 2020-03-07, etc, etc, etc,
}
Where key is always a positive digit and value is a date.
Problem here that I want to extract keys ( by akeys), and then somehow get the biggest key and compare it with number_of_episodes(positive integer).
But it says that I can’t use arrays in check constraint.
Question is -is it possible to extract somehow biggest key from HSTORE as an integer and use it in check constraint afterwards?
Thank you.
alter table archives_seasonmodel
add constraint test
check (max((unnest(akeys(episodes))) <= number_of_episodes ))
This doesn’t work.
This works for me in PostgreSQL 10:
# create table tvseries
(number_of_episodes int,
episodes hstore,
check (number_of_episodes >= all (akeys(episodes)::int[]))
);
CREATE TABLE
# insert into tvseries values (2, '1=>"a", 2=>"b"');
INSERT 0 1
# insert into tvseries values (1, '1=>"a", 2=>"b"');
ERROR: new row for relation "tvseries" violates check constraint "tvseries_check"
DETAIL: Failing row contains (1, "1"=>"a", "2"=>"b").
# insert into tvseries values (2, '1=>"a"');
INSERT 0 1
# select * from tvseries;
number_of_episodes | episodes
--------------------+--------------------
2 | "1"=>"a", "2"=>"b"
2 | "1"=>"a"
(2 rows)
This answer outlines a couple ways you can go about this. The first is to use the intarray extension and it's sort_desc function, but I think the better approach here is to use a custom function.
testdb=# create extension hstore;
CREATE EXTENSION
testdb=# create table tt0(h hstore, max_n bigint);
CREATE TABLE
testdb=# CREATE OR REPLACE FUNCTION array_greatest(anyarray)
RETURNS anyelement LANGUAGE SQL AS $$
SELECT max(x) FROM unnest($1) as x;
$$;
CREATE FUNCTION
testdb=# alter table tt0 add check((array_greatest(akeys(h)::integer[]))<=max_n);
ALTER TABLE
testdb=# insert into tt0 select hstore(ARRAY[['1','asdf'],['3','fdsa']]), 2;
ERROR: new row for relation "tt0" violates check constraint "tt0_check"
DETAIL: Failing row contains ("1"=>"asdf", "3"=>"fdsa", 2).
testdb=# insert into tt0 select hstore(ARRAY[['1','asdf'],['2','fdsa']]), 2;
INSERT 0 1
testdb=# select * from tt0
testdb-# ;
h | max_n
--------------------------+-------
"1"=>"asdf", "2"=>"fdsa" | 2
(1 row)
testdb=# \d tt0
Table "public.tt0"
Column | Type | Collation | Nullable | Default
--------+--------+-----------+----------+---------
h | hstore | | |
max_n | bigint | | |
Check constraints:
"tt0_check" CHECK (array_greatest(akeys(h)::integer[]) <= max_n)

Create function in postgresql to update column values from a table with preferred values and aliases

I want to create a function that will update a column of type varchar to a preferred string that is referenced in the column of another table to help me clean this column more iteratively.
CREATE TABLE big_table (
mn_uid NUMERIC PRIMARY KEY,
user_name VARCHAR
);
INSERT INTO big_table VALUES
(1, 'DAVE'),
(2, 'Dave'),
(3, 'david'),
(4, 'Jak'),
(5, 'jack'),
(6, 'Jack'),
(7, 'Grant');
CREATE TABLE nameKey_table (
nk_uid NUMERIC PRIMARY KEY,
correct VARCHAR,
wrong VARCHAR
);
INSERT INTO nameKey_table VALUES
(1, 'David', 'Dave_DAVE_dave_DAVID_david'),
(2, 'Jack', 'JACK_jack_Jak_jak');
I want to perform the following procedure:
UPDATE big_table
SET user_name = (SELECT correct
FROM nameKey_table
WHERE wrong
LIKE '%DAVE%')
WHERE user_name = 'DAVE';
but looped over each user_name in big_table so that I have a function that can do something like this:
UPDATE big_table SET user_name = corrected_name_fn();
Here is my attempt to do something like this but I can't seem to get it to work:
CREATE FUNCTION corrected_name_fn() RETURNS VARCHAR AS $$
DECLARE entry RECORD;
DECLARE correct_name VARCHAR;
BEGIN
FOR entry IN SELECT DISTINCT user_name FROM big_table LOOP
EXECUTE 'SELECT correct
FROM nameKey_table
WHERE wrong
LIKE ''%$1%'''
INTO correct_name
USING entry;
RETURN correct_name;
END LOOP;
END;
$$ LANGUAGE plpgsql;
I want the final output in big_table to be:
| mn_uid | user_name |
| 1 | 'David' |
| 2 | 'David' |
| 3 | 'David' |
| 4 | 'Jack' |
| 5 | 'Jack' |
| 6 | 'Jack' |
| 7 | 'Grant' |
I realize rows 6 and 7 provide two unique cases that I want to build into the function with IF ELSE statements.
If user_name is in nameKey_table.correct, go to next
If user_name is not in nameKey_table.correct or does not match a string in nameKey_table.wrong, leave as is.
Thanks for any help on this!!
It sounds like you want a trigger on the table. Here is my suggestion:
CREATE OR REPLACE FUNCTION tf_fix_name() RETURNS TRIGGER AS
$$
DECLARE
corrected_name TEXT;
BEGIN
SELECT correct INTO corrected_name FROM nameKey_table WHERE expression ~* NEW.user_name;
IF FOUND THEN
NEW.user_name := corrected_name;
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TEMP TABLE big_table (
mn_uid INT PRIMARY KEY,
user_name TEXT NOT NULL
);
CREATE TRIGGER trigger_fix_name
BEFORE INSERT
ON big_table
FOR EACH ROW
EXECUTE PROCEDURE tf_fix_name();
CREATE TEMP TABLE nameKey_table (
nk_uid INT PRIMARY KEY,
correct TEXT NOT NULL,
expression TEXT NOT NULL
);
INSERT INTO nameKey_table VALUES
(1, 'David', '(dave|david)'),
(2, 'Jack', '(jack|jak)');
INSERT INTO big_table VALUES
(1, 'DAVE'),
(2, 'Dave'),
(3, 'david'),
(4, 'Jak'),
(5, 'jack'),
(6, 'Jack'),
(7, 'Grant');
SELECT * FROM big_table;
+--------+-----------+
| mn_uid | user_name |
+--------+-----------+
| 1 | David |
| 2 | David |
| 3 | David |
| 4 | Jack |
| 5 | Jack |
| 6 | Jack |
| 7 | Grant |
+--------+-----------+
(7 rows)
Note: I think you can do what you want a lot easier with a case insensitive regular expression. And I also changed your primary keys to INTs. Not sure why they are numerics, but it doesn't really change the solutions. My solution was developed and tested on PostgreSQL 9.6.
You don't need a function; you can just update one table from the contents of another table:
UPDATE big_table dst
SET user_name = src.correct
FROM nameKey_table src
WHERE src.wrong LIKE '%' || dst.user_name || '%'
AND dst.user_name <> src.correct -- avoid idempotent updates
;
And if you need performance, dont rely on the LIKE operator, it cannot use indexes for leading %. Instead, use a lookup-table with one entry per row:
CREATE TABLE bad_spell (
correct VARCHAR,
wrong VARCHAR PRIMARY KEY -- This will cause an unique index to be created.
);
INSERT INTO bad_spell VALUES
('David', 'Dave')
,('David', 'DAVE')
,('David', 'dave')
,('David', 'DAVID')
,('David', 'david')
,('Jack', 'JACK')
,('Jack', 'jack')
,('Jack', 'Jak')
,('Jack', 'jak')
;
-- This indexes could be temporary
CREATE INDEX ON big_table(user_name);
-- EXPLAIN
UPDATE big_table dst
SET user_name = src.correct
FROM bad_spell src
WHERE dst.user_name = src.wrong
AND dst.user_name <> src.correct -- avoid idempotent updates
;
SELECT* FROM big_table
;

Postgres Insert Into On conflict do update

I want to insert data from one table ("tmp") into another ("tbla"), using the on conflict do update-feature.
My Code:
INSERT INTO tbla (id, col1, col2, col3)
SELECT id, col1, col2, col3 FROM tmp
ON CONFLICT on constraint pkey_tbla DO UPDATE SET col1=tmp.col1 FROM tmp;
DROP TABLE tmp;
This code gives me back an Syntax-Error at "FROM tmp;"
Without FROM there is the ERROR: missing FROM-clause entry for table "tmp"
Any suggestions on what I'm doing wrong?
DB-Server is running on localhost at a windows 7-machine with postgres 9.5
Documentation "Note that the special excluded table is used to reference values originally proposed for insertion" https://www.postgresql.org/docs/9.5/static/sql-insert.html
Fix : ... DO UPDATE SET col1=EXCLUDED.col1;
x=> select * from tbla;
id | col1
----+------
1 | 2
2 | 3
(2 rows)
x=> truncate tmp;
TRUNCATE TABLE
x=> insert into tmp(id,col1) values (1,42);
INSERT 0 1
x=> INSERT INTO tbla(id,col1) SELECT id,col1 FROM tmp -- wrap line
ON CONFLICT (id) DO UPDATE SET col1=EXCLUDED.col1;
INSERT 0 1
sh161119=> select * from tbla;
id | col1
----+------
2 | 3
1 | 42
(2 rows)

T-SQL One column in multiple columns select query

I have a simple problem that I have not been able to find a solution to and I'm hoping someone on StackOverflow can help.
I currently have an example query as shown below
SELECT ID
, ColumnName
FROM Table
If I run this query I get the following result:
==================
ID | ColumnName
------------------
1 | One_Two_Three
2 | Four_Five_Six
==================
The result I'm after is as follows:
========================
ID | Col1 | Col2 | Col3
------------------------
1 | One | Two | Three
2 | Four | Five | Six
========================
Your assistence is appreciated.
Have a look at this example
DECLARE #Table1 TABLE
([ID] int, [ColumnName] varchar(13))
INSERT INTO #Table1
([ID], [ColumnName])
VALUES
(1, 'One_Two_Three'),
(2, 'Four_Five_Six')
;WITH Vals AS (
SELECT *,
CAST('<d>' + REPLACE([ColumnName], '_', '</d><d>') + '</d>' AS XML) ColumnValue
FROM #Table1
)
SELECT v.*,
A.B.value('.', 'varchar(max)')
FROM Vals v CROSS APPLY
ColumnValue.nodes('/d') A(B)
SQL Fiddle DEMO