How to upsert when using data from a sub-query (Postgres) - sql

I have two tables:
assignments {recceptacleId, assignedCarrier}
rls_permissions {receptacleId, rlsUserId}
An assignment in this context is any receptacle to airline carrier relationship.
Whenever a new assignment comes into the assignments table, I'd like to upsert (insert if new row or update if it's an existing receptacle being assigned to a new airline carrier) my rls_permissions table.
The issue I'm having with upsert, specifically ON CONFLICT ON CONSTRAINT, is that my insert statement contains a sub-query for the data to be inserted and therefore I'm not sure how to write the DO UPDATE SET part of the statement
I've tried using 'excluded' to try and single out the assignedCarrier that I want to update based on the previous conflict however I keep receiving "ERROR: column excluded.receptacleId does not exist"
My pkey looks like this:
CREATE UNIQUE INDEX rls_permissions_pkey ON rls_permissions("receptacleId" text_ops);
Dummy data could be:
receptacleID assignedCarrier
aaaaaaaaaa00 AA
Where AA is "American Airlines"
INSERT INTO rls_permissions ("receptacleId","rlsUserId")
SELECT DISTINCT assignments."receptacleId", assignments."assignedCarrier"
FROM assignments
ON CONFLICT ON CONSTRAINT rls_permissions_pkey
DO UPDATE SET "rlsUserId" = (SELECT DISTINCT assignments."assignedCarrier"
FROM assignments
WHERE assignments."receptacleId" = excluded."receptacleId");
The excepted result is that if no conflict, the data returned from the sub-query is inserted into a new row on the permissions table.
If there is a conflict, I'd like to update ONLY the newly assigned carrier, and not update or insert a new line since that receptacle already exists.

You don't need a subquery in the UPDATE part. You can access the values for the INSERT part through the excluded keyword.
INSERT INTO rls_permissions ("receptacleId","rlsUserId")
SELECT DISTINCT assignments."receptacleId", assignments."assignedCarrier"
FROM assignments
ON CONFLICT ON CONSTRAINT rls_permissions_pkey
DO UPDATE SET "rlsUserId" = excluded."rlsUserId";
the reference to excluded."rlsUserId" refers to the value that would have been inserted into the column rlsUserId and thus it's the value retrieved through assignments."assignedCarrier" from your SELECT statement.

Related

Is there an elegant way to deal with inserting rows with duplicate identifiers?

I'm trying to insert some rows into my table that have the same unique identifier, but all the other fields are different (the rows represent points on a map, and they just happen to have the same name). The final result I'd like to end up with is to somehow modify the offending rows to have unique identifiers (adding on some incrementing number to the identifier, like "name0", "name1", "name2", etc.) during the insertion command.
I'm aware of Postgres's recent addition of "ON CONFLICT" support, but it's not quite what I'm looking for.
According to the Postgres 9.6 Documentation:
The optional ON CONFLICT clause specifies an alternative action to raising a unique violation or exclusion constraint violation error. For each individual row proposed for insertion, either the insertion proceeds, or ... the alternative conflict_action is taken. ...ON CONFLICT DO UPDATE updates the existing row that conflicts with the row proposed for insertion as its alternative action.
What I would like to do is 1) either modify the offending row or the insertion itself and 2) proceed with the insertion (instead of replacing it with an update, like the ON CONFLICT feature does). Is there an elegant way of accomplishing this? Or am I going to need to write something more complex?
You can do this:
create table my_table
(
name text primary key,
some_column varchar
);
create sequence my_table_seq;
The sequence is used to assign a unique suffix to the new row's PK column.
The "insert on conflict insert modified" behaviour can be done like this:
with data (name, some_column) as (
values ('foo', 'bar')
), inserted as (
insert into my_table
select *
from data
on conflict (name) do nothing
returning *
)
insert into my_table (name, some_column)
select concat(name, '_', nextval('my_table_seq')), some_column
from data
where not exists (select 1 from inserted);
The first time you insert a value into the PK column, the insert (in the CTE "inserted") just proceeds. The final insert won't insert anything because the where not exists () prevents that as the inserted returned one row.
The second time you run this, the first insert won't insert anything, and thus the second (final) insert will.
There is one drawback though: if something was inserted by the "inserted" CTE, the the overall statement will report "0 rows affected" because the final insert is the one "driving" this information.

SQL Update statement that deletes rows if key clash

I have a table that has two columns Hotel_Guest_ID and Guest_ID that links Guest records to the Hotel details of that guest. The table has the constraint that each pair needs to be unique.
I now have a second table of Prime_ID and Duplicate_ID that was generated after cleaning up the Guest table of duplicates. I would like to go through the Booking table and if the Hotel.Guest_ID is found as a Duplicate_ID, to then replace it with Prime_ID.
update b
set h.Guest_ID = gd.Prime_ID
from Hotel as h
join Guest_Duplicates as gd
on h.Guest_ID = gd.Duplicate_ID
However this fails as often a Prime_ID already has a record with a specific Guest, at which point I want to just delete this row instead of updating it.
Is there a nice way to do this in a single pass or would I have to delete potential clash rows first, then update in a second query?
What you are looking for is a MERGE statement. You can insert, update and delete using a single statement. Here's an example -
MERGE Table1 AS t1
USING Table2 AS t2
ON t1.GuestID = t2.DuplicateID
WHEN MATCHED AND (any condition)
THEN DELETE
WHEN MATCHED
THEN UPDATE SET (assign statement)
WHEN NOT MATCHED
THEN
INSERT(column names)
VALUES(values to be inserted);

Using Trigger to insert record in another table, AND write back the ID into original record ..?

I have a simple trigger setup, which is used to insert records into a RentJournal table, whenever there is a record inserted in the UnitAGA table.
The RentJournal table has a primary key ID column named RentJournalID, which is auto incrementing. The UnitAGA table also has a nullable foreign key column named RentJournalID, which links each UnitAGA entry, to its corresponding entry in RentJournal table (which is inserted through the Trigger below).
Problem is that currently this Trigger is only inserting values into RentJournal table. But now I want to also fetch the ID assigned for each RentJournal entry through this Trigger, and write that into the corresponding UnitAGA record, whose insert actually triggered the Trigger in the first place. How do I do this ?
The Trigger code as of right now is this:
USE [RentDB]
GO
ALTER TRIGGER [RTS].[InsertRentJournalEntry]
ON [RTS].[UnitAGA]
AFTER INSERT
AS
BEGIN
INSERT INTO RTS.RentJournal
(UnitId, AdjustmentType, EffectiveDate, ReferenceFormNo)
SELECT
UnitId, 'AGA', EffectiveDate, ReferenceFormNo FROM inserted
END
Have a look at the INSERT logical table that is available in insert triggers:
DML triggers use the deleted and inserted logical (conceptual) tables. They are structurally similar to the table on which the trigger is defined, that is, the table on which the user action is tried. The deleted and inserted tables hold the old values or new values of the rows that may be changed by the user action. For example, to retrieve all values in the deleted table, use: SELECT * FROM deleted
http://technet.microsoft.com/en-us/library/ms189799.aspx
Then use ##IDENTITY to get the value of the identity column on your RentJournal table.
So you should be able to do something like:
update INSERTED set RentJournalID = ##IDENTITY

Creating a Trigger which will insert record in a table on update of another table

Suppose I have tables T1 and T2
Columns of T1 -->Value
Columns of T2 -->OldValue NewValue
What I require is a trigger which will insert a record in T2 on updation of T1 , I need to know the old value and new value also , I have never used triggers before , so can any help me with this , how do I go about creating this trigger.Is it possible ,thanks.
Well, you start writing a trigger with CREATE TRIGGER:
CREATE TRIGGER NameOfTheTriggerPlease
…
The table that should trigger the additional action is T1 so the trigger should be defined ON that table:
CREATE TRIGGER T1OnUpdate /* that's just an example,
you can use a different name */
ON T1
…
The action that the trigger should be invoked on is UPDATE and the timing is AFTER the update, so…
CREATE TRIGGER T1OnUpdate
ON T1
AFTER UPDATE
…
Now's the time to introduce the body of the trigger, i.e. the statements that should actually be executed by the trigger. You introduce the body with the AS keyword followed by the statements themselves.
In your case, there would be just one statement, INSERT, which is obvious. What's not so obvious is how we are going to access the old and the new values. Now, SQL Server offers you two virtual tables, INSERTED and DELETED, and you can easily guess that the former contains all the new values and the latter the old ones.
These tables have the same structure as the table the trigger is assigned to, i.e. T1. They only contain rows that were affected by the particular UPDATE statement that invoked the trigger, which means there may be more than one. And that, in turn, means that you need to have some primary key or a unique column (or a set of columns) in your T1 table that you can use in the trigger to match deleted and inserted rows. (In fact, you might also need your T2 table to have a column that would reference the T1's primary key, so you could later establish which row of T1 had which values stored in T2.)
For the purposes of this answer, I'm going to assume that there's a primary key column called PK and a foreign key column of the same name in T2. And the INSERT statement then might look like this:
CREATE TRIGGER T1OnUpdate
ON T1
AFTER UPDATE
AS
INSERT INTO T2 (PK, OldValue, NewValue)
SELECT i.PK, i.Value, d.Value
FROM INSERTED i INNER JOIN DELETED d ON i.PK = d.PK
One last (but not least) thing to remember: the entire CREATE TRIGGER statement should be the only one in the batch, i.e. there should be no statements preceding the CREATE TRIGGER keywords (but you can put comments there) and, likewise, everything after the AS keyword is considered part of the trigger's body (but you can put the GO delimiter to indicate the end of the statement if you are running the script in SQL Server Management Studio, for instance).
Useful reading:
CREATE TRIGGER (Transact-SQL)
I'm not going to build the whole thing for you (no fun, right?) but I can point you in the right direction
create trigger logUpdate
on T1
After update
as
begin
insert into T2...
--here is just an example
select * from deleted --the DELETED table contains the OLD values
select * from inserted --the INSERTED table contains the NEW values
end
remember that DELETED and INSERTED are internal tables that contains old and new values. On a update trigger, they both exist. On a insert trigger, DELETED will be null because there is nothing being delete. Same logic on a delete trigger, the INSERTED will be empty
EDIT:
answering your question: no matter how many fields you update, your DELETED and INSERTED tables you have all the columns of all the rows affected. Of course, if you update only one column, all the other will have the same value on DELETED and INSERTED
create trigger T_UPD_T1
on T1 FOR update
as
insert into T2 select deleted.value, inserted.value from inserted, deleted

INSTEAD OF UPDATE TRIGGER for a table with foreign key

I get this error:
Cannot create INSTEAD OF DELETE or INSTEAD OF UPDATE TRIGGER 'trig_Income_Updater' on table 'MYBUDGET.tbl_Income'. This is because the table has a FOREIGN KEY with cascading DELETE or UPDATE.
I can use 'FOR UPDATE'. but how to make it to ignore the original update ?
To ignore the origional update you will need to utilize a RAISERROR and then ROLLBACK.
Here is an example under the "Using a DML AFTER trigger to enforce a business rule between the PurchaseOrderHeader and Vendor tables" section
Just update the fields you want and then "undo" the original update using the "inserted" and "deleted" temporary tables that are provided to the trigger.
For example (untested):
--Do the stuff you want
UPDATE table SET fields = values WHERE some condition
--Undo the original update (minus anything you WANT changed above)
UPDATE table SET unchangingfield = deleted.unchangingfield WHERE ID = deleted.ID
The "inserted" table will contain the new values, and the "deleted" table contains the values that are being changed. You can join, query and otherwise treat them as though they were actual tables.