Using an id returned from an insert in a with statement in postgresql - postgresql

Say that you have the following table structure, that you like wikipedia have the identity and state of a page stored in different tables:
create table endUsers (
uuid UUID primary key,
created timestamptz default now()
);
create table endUserRevisions (
id bigserial primary key,
endUser UUID not null references endUsers,
modified timestamptz default now(),
modifiedBy UUID not null references portalUsers,
name text not null,
company text not null,
email text not null
);
alter table endUsers add column
latestRevision bigint not null references endUserRevisions;
And that you then want to insert a completely new user into this database like:
with lastID as (
insert into endUserRevisions (endUser, name, company, email)
values ('08e7882c-7596-43d1-b4cc-69f855210d72', 'a', 'b', 'c') returning id)
insert into endUsers (uuid, latestRevision)
values ('08e7882c-7596-43d1-b4cc-69f855210d72', lastID);
-- or
with revision as (
insert into endUserRevisions (endUser, name, company, email)
values ('08e7882c-7596-43d1-b4cc-69f855210d72', 'a', 'b', 'c') returning *)
insert into endUsers (uuid, latestRevision)
values ('08e7882c-7596-43d1-b4cc-69f855210d72', revision.id);
Both these variants fail with either
column "lastid" does not exist
or
missing FROM-clause entry for table "last"

The reason why the fail is because each subquery is accessable to the surrounding context as a table, not as a plain value. In other words it must be accessed using a select statement like:
with revision as (
insert into endUserRevisions (endUser, name, company, email)
values ('08e7882c-7596-43d1-b4cc-79f855210d76', 'a', 'b', 'c') returning id)
insert into endUsers (uuid, latestRevision)
values ('08e7882c-7596-43d1-b4cc-79f855210d76', (select id from revision));
-- or
with revision as (
insert into endUserRevisions (endUser, name, company, email)
values ('08e7882c-7596-43d1-b4cc-79f855210d74', 'a', 'b', 'c') returning id)
insert into endUsers (uuid, latestRevision)
select '08e7882c-7596-43d1-b4cc-79f855210d74', revision.id from revision;

Related

PostrgreSQL ForeignKeyViolation

I am attempting to insert some data into my database via a lambda function. I am getting the following error ForeignKeyViolation: insert or update on table "address" violates foreign key constraint "address_id_fkey"
I understand that this is because my address table has a foreign key linking it to the clients table, and the keys are not matching.
Is there a way to format my tables so that I can input my client data and address data together? Or will I need to input the client data first, then retrieve the id and use it to input the address data.
Currently I am running the following two functions.
postgres_insert_query = "INSERT INTO clients (name, phone, contact) VALUES ('{0}','{1}','{2}')".format(data['name'], data['phone'], data['contact'])
postgres_insert_query = "INSERT INTO address (line1, city, state, zip) VALUES ('{0}','{1}','{2}', {3})".format(address['line1'], address['city'], address['state'], address['zip'])
Even if no address data is present I would still like to create a row for it (with the correct foreign key).
use DEFERRABLE foreign key constraint. Then wrap you function into a transaction.
CREATE temp TABLE pktable (
id INT4 PRIMARY KEY,
other INT4
);
CREATE temp TABLE fktable (
id INT4 PRIMARY KEY,
fk INT4 REFERENCES pktable DEFERRABLE INITIALLY DEFERRED
);
BEGIN;
INSERT INTO fktable VALUES (100, 200);
INSERT INTO pktable VALUES (200, 500);
COMMIT;
Postgres allows DML operations within a CTE. Doing so will allow you to insert into both tables in a single statement while allowing auto-generation of both ids. The following is a Postgres implementation. See demo.
with thedata(name, phone, contact, line1, city, state, zip) as
( values ('client 1', 'ev4 4213', 'andy','614 a', 'some city;','that state','11111'))
, theinsert (cli_id) as
( insert into clients(name, phone, contact)
select name, phone, contact
from thedata
returning cli_id
)
insert into addresses(cli_id, line1, city, state, zip)
select cli_id, line1, city, state, zip
from theinsert
cross join thedata;
Unfortunately I do not know your obscurification (Orm) language but perhaps something like:
pg_query = "with thedata( {0} name, {1} phone, {2} contact, {3} line1, {4} city, {5} state, {6} zip) as
, theinsert (cli_id) as
( insert into clients(name, phone, contact)
select name, phone, contact
from thedata
returning cli_id
)
insert into addresses(cli_id, line1, city, state, zip)
select cli_id, line1, city, state, zip
from theinsert
cross join thedata".format(data['name'], data['phone'], data['contact']
, address['line1'], address['city'], address['state'], address['zip']);

Passing UUID of one table to another table as a Foreign key value in PostgreSQL

I have table Employee in Postgres:
drop table if exists employee;
create table employee (
id uuid default uuid_generate_v4 () primary key,
first_name varchar not null,
last_name varchar not null
);
And another table salary :
drop table if exists salary;
create table salary (
check_id uuid default uuid_generate_v4 () primary key,
salary int not null,
employee_id uuid references employee (id)
);
employee_id is the foreign key to id in the Employee table, but I don't understand how to insert a value inside employee_id since UUID is unique.
I am inserting values into Employee table:
insert into employee (first_name, last_name, email, code) values ( 'jonh', 'smith', 'jonh#example.com', '1');
And then if I try insert values into salary table:
insert into salary (salary ) values ('1000');
Then select command will return employee_id value empty.
But if I make it default uuid_generate_v4 (), then result is: Key (employee_id)=(c4ccd745-02ba-4a0e-8586-32e3c6a2b84a) is not present in table "employee".
I understand that because employee_id is a foreign key it should match with uuid in employee, but since uuid is mostly unique, how can I make it work?
You have to use the uuid that was inserted into the employee table. You can do this with a CTE in a single statement:
WITH new_employee AS (
INSERT INTO employee (first_name, last_name, email, code)
VALUES ('jonh', 'smith', 'jonh#example.com', '1')
RETURNING id
)
INSERT INTO salary (salary, employee_id)
SELECT 1000, id
FROM new_employee;

Query JSONB column using joins to filter by subquery referencing outer query

I need to analyze survey data (stored in records) where a question can have a choice of options. My goal is to identify the answers given that were NOT within the range of allowed options for this question. However, my query returns everything (I suspect a subquery) and I don't know how to fix it.
Schema
The records stores its data in the data JSONB column. There, the keys are question UIDs, e.g. uid00000006 has the answer option1. option1 is a choice to select.
(Not all questions need to have a dropdown, so some other value is fine such as 42.)
{"uid00000006": {"value": "option1"}, "uid00000008": {"value": 42}}
A question optionally has a reference to a optionset (the dropdown) which has a range of optionvalues (the values of the dropdown) , e.g. option1, option2, option3 etc.
create table record
(
recordid bigint not null primary key,
uid varchar(11) unique,
data jsonb default '{}'::jsonb not null
);
create table question
(
questionid bigint not null primary key,
uid varchar(11) not null unique,
optionsetid bigint
);
create table optionset
(
optionsetid bigint not null primary key,
uid varchar(11) not null unique
);
create table optionvalue
(
optionvalueid bigint not null primary key,
uid varchar(11) not null unique,
code varchar(230) not null,
optionsetid bigint
);
-- create optionset
INSERT INTO optionset (optionsetid, uid) VALUES (1, 'uid00000001');
-- insert optionvalues into optionset
INSERT INTO optionvalue (optionvalueid, uid, code, optionsetid) VALUES (100, 'uid00000002', 'option1', 1);
INSERT INTO optionvalue (optionvalueid, uid, code, optionsetid) VALUES (101, 'uid00000003', 'option2', 1);
INSERT INTO optionvalue (optionvalueid, uid, code, optionsetid) VALUES (102, 'uid00000004', 'option3', 1);
INSERT INTO optionvalue (optionvalueid, uid, code, optionsetid) VALUES (103, 'uid00000005', 'option4', 1);
-- insert questions
INSERT INTO question (questionid, uid, optionsetid) VALUES (1001, 'uid00000006', 1);
INSERT INTO question (questionid, uid, optionsetid) VALUES (1002, 'uid00000007', 1);
INSERT INTO question (questionid, uid, optionsetid) VALUES (1003, 'uid00000008', NULL);
-- insert records
INSERT INTO record (recordid, uid, data) VALUES (10001, 'uid00000009', '{"uid00000006": {"value": "option1"}, "uid00000008": {"value": 42}}'::jsonb);
INSERT INTO record (recordid, uid, data) VALUES (10002, 'uid00000010', '{"uid00000006": {"value": "option2"}}'::jsonb);
INSERT INTO record (recordid, uid, data) VALUES (10003, 'uid00000011', '{"uid00000006": {"value": "UNMAPPED"}}'::jsonb);
My query
My drafted query is:
SELECT r.uid AS record_uid,
key AS question_uid,
os.uid AS optionset_uid,
value ->> 'value' AS value
FROM record r, JSONB_EACH(r.data)
JOIN question q ON q.uid = key
JOIN optionset os ON q.optionsetid = os.optionsetid
WHERE q.optionsetid IS NOT NULL
AND value::varchar NOT IN (SELECT DISTINCT code FROM optionvalue WHERE optionsetid = q.optionsetid)
;
DBFiddle
Problem
The query above returns all records instead only one. In reference to the sample data, the expected result would be to return only the record where the value is UNMAPPED (meaning it is the record where an answer was given that is not "valid").
You should change value::varchar NOT IN to value ->> 'value' NOT IN
SELECT
r.uid AS record_uid,
key AS question_uid,
os.uid AS optionset_uid,
value ->> 'value' AS value
FROM
record r, jsonb_each(r.data)
JOIN question q ON q.uid = key
JOIN optionset os ON q.optionsetid = os.optionsetid
WHERE
q.optionsetid IS NOT NULL
AND value ->> 'value' NOT IN (SELECT DISTINCT code FROM optionvalue WHERE optionsetid = q.optionsetid);

Running A FOREACH after a CTE in a PostgreSQL procedure?

I have a PLPGSQL procedure that:
A) Inserts an employee into an Employee table.
B) Also insert the generated serial eid into another Manager table.
C) Finally, the procedure also dictates an array of course_areas that I would like to insert in a specialize table.
Procedure where C and D array are the course_areas
CALL add_employee('Athena', '22222222', 'athena#outlook.com', '2012-12-12', '111', 'instructor', 300.0, NULL,
'{C,D}'::text[]);
Employees
CREATE TABLE Employees (
eid SERIAL PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL,
email TEXT NOT NULL,
join_date DATE NOT NULL,
address TEXT NOT NULL,
depart_date DATE
);
Managers
CREATE TABLE Managers (
eid INT PRIMARY KEY,
monthly_salary DECIMAL(10,2) NOT NULL,
FOREIGN KEY (eid) REFERENCES Employees(eid)
ON DELETE CASCADE
);
Specializes
CREATE TABLE Specializes (
eid INT NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (eid, name),
FOREIGN KEY (eid) REFERENCES Employees(eid)
on DELETE CASCADE,
FOREIGN KEY (name) REFERENCES Course_areas(name)
on DELETE CASCADE
);
procedure.sql
CREATE OR REPLACE PROCEDURE add_employee (name TEXT, phone TEXT, email TEXT, joinDate DATE, address TEXT, category TEXT, monthlySalary DECIMAL(10,2) default NULL, hourlySalary DECIMAL(10,2) default NULL, courseAreas TEXT[] default NULL)
...
WITH FIRST AS (
INSERT INTO Employees(
name, phone, email, join_date, address
)
VALUES
(
name, phone, email, joinDate, address
) RETURNING eid
),
SECOND AS (
INSERT INTO Full_time_instructors(eid, monthly_salary)
SELECT
eid,
monthlySalary
FROM
ROWS RETURNING eid
)
FOREACH area IN ARRAY courseAreas
LOOP
RAISE NOTICE '%', area; -- should print "C" and "D"
END LOOP;
Error
ERROR: syntax error at or near "FOREACH"
LINE 27: FOREACH area IN ARRAY courseAreas
I can get A) and B) to work.
How can I use FOREACH to iterate through my courseAreas that I pass to the procedure such that I can insert each course area and the eid into a Specialize table?
The FOREACH is a PLPGSQL control structure whereas the CTE is a SQL feature. You can't mix these. Instead of using CTE you can simply perform the first statement, using the RETURNING clause to retrieve the eid to a local variable. You can then use this variable with the subsequent statement and inside your FOREACH loop.
This question gives an overview of getting the new serial value using returning: Get default serial value after INSERT inside PL/pgSQL.

Postgres Dynamic Query

I have scenario were I have a master table which stores db table name and column name, I need to build dynamic query based on that.
CREATE TABLE MasterTable
(
Id int primary key,
caption varchar(100),
dbcolumnname varchar(100),
dbtablename varchar(100)
);
CREATE TABLE Engineers
(
Id int primary key,
Name varchar(100),
Salary BigInt
);
CREATE TABLE Executives
(
Id int primary key,
Name varchar(100),
Salary BigInt
);
CREATE TABLE Manager
(
Id int primary key,
Name varchar(100),
Salary BigInt
);
INSERT INTO Manager(Id, Name, Salary)
VALUES(1, 'Manager 1', 6000000);
INSERT INTO Executives(Id, Name, Salary)
VALUES(1, 'Executive 1', 6000000);
INSERT INTO Engineers(Id, Name, Salary)
VALUES(1, 'Engineer 1', 6000000);
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (1, 'Name', 'name', 'Engineers');
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (2, 'Name', 'name', 'Manager');
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (3, 'Name', 'name', 'Executives');
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (4, 'Salary', 'Salary', 'Engineers');
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (5, 'Salary', 'Salary', 'Manager');
INSERT INTO MasterTable(Id, caption, dbcolumnname, dbtablename)
VALUES (6, 'Salary', 'Salary', 'Executives');
I want to build a stored procedure which accepts caption and Id and give result back based on dbcolumnname and dbtablename. For example if I pass Salary,Name as caption and Id as 1, stored procedure should be query of dbcolumn and dbtable, something like below.
Select Id as ID, name as Value from Engineers
UNION
Select Id as ID, name as Value from Manager
UNION
Select Id as ID, name as Value from Executives
UNION
Select Id as ID, Salary as Value from Executives
UNION
Select Id as ID, Salary as Value from Engineers
UNION
Select Id as ID, Salary as Value from Manager
I have heard of dynamic sql, can that be used here?
Fiddle
EDIT :: I got one dynamic query which builds union statement to get the output, however problem is i am not able to escape double quotes. Below is the query and Error
Query :
DO
$BODY$
BEGIN
EXECUTE string_agg(
format('SELECT %I FROM %I', dbcolumnname, dbtablename),
' UNION ')
FROM MasterTable;
END;
$BODY$;
Error:
ERROR: relation "Engineers" does not exist
LINE 1: SELECT name FROM "Engineers" UNION SELECT name FROM "Manager...
I'd like to suggest an alternative way of achieving what you want. That is, using PostgreSQL inheritance mechanism.
For instance:
CREATE TABLE ParentTable (
Id int,
Name varchar(100),
Salary BigInt
);
ALTER TABLE Engineers INHERIT ParentTable;
ALTER TABLE Executives INHERIT ParentTable;
ALTER TABLE Manager INHERIT ParentTable;
SELECT Id, Salary AS value FROM ParentTable
UNION
SELECT Id, Name AS value FROM ParentTable;
Now if you want to use MasterTable in order to restrict the set of tables used, you can do it as follows:
SELECT Id, Name AS value
FROM ParentTable
INNER JOIN pg_class ON parenttable.tableoid = pg_class.oid
INNER JOIN MasterTable ON LOWER(dbtablename) = LOWER(relname)
UNION
SELECT Id, Salary AS value
FROM ParentTable
INNER JOIN pg_class ON parenttable.tableoid = pg_class.oid
INNER JOIN MasterTable ON LOWER(dbtablename) = LOWER(relname)
However, you can not arbitrarily restrict the set of columns to retrieve from one table to another with this technique.
Table names and column names are case insensitive in SQL, unless they are quoted in double quotes. Postgres does this by folding unquoted identifiers to lower case.
So, your DDL:
CREATE TABLE MasterTable
(
Id int primary key,
caption varchar(100),
dbcolumnname varchar(100),
dbtablename varchar(100)
);
Will be interpreted by Postgres as
CREATE TABLE mastertable
(
id int primary key,
caption varchar(100),
dbcolumnname varchar(100),
dbtablename varchar(100)
);
You can avoid case folding by quoting the names:
CREATE TABLE "MasterTable"
(
"Id" int primary key,
caption varchar(100),
dbcolumnname varchar(100),
dbtablename varchar(100)
);
The %I format-specifier (internally uses quote_ident()) adds quotes to its argument (when needed)
, so the query asks for "MasterTable" when only mastertable is present in the schema.
But, it is easyer to just avoid MixedCase identifiers,