How to maintain a postgreSQL lock from a trigger "before update" through the update operation itself - postgresql

I apologize if this is an answered question, I did some research, and I couldn't find an answer.
I'm maintaining a folder/file like structure in my code where I have ordered items that cascade order changes on update and deletion operations. However, these triggers need to both lock rows to ensure that the order changes are completed and continue to lock through the completion of the operation
The updating process is relatively simple. This is the governing pseudo-code for the entire operation:
check if pg_trigger_depth() >= 1
return because this was a cascaded update from a trigger
lock the table for update on items with the old folder_parent_id
lock the table for update on items with the new folder_parent_id
update the old rows setting order_number -= 1 where the order_number is > the old order_number, and the folder_parent_id is the same as the old one
update the new rows setting order_number +=1 where the order_number is >= the new order_number and the folder_parent_id is the same as the new one
allow the update operation to go through (setting the order_number/folder_parent_id of this row to its new location)
release the lock for update on items with the old folder_parent_id
release the lock for update on items with the new folder_parent_id
If the lock is released before the actual operation goes through, this sort of race condition can happen. In this sample problem, two updates are being called simultaneously:
Given children of a folder: a(0), b(1), c(2), d(3), e(4)
the letters are the identifying properties and the numbers are the order numbers
we want to run these operations: c(2 -> 1), d(3 -> 0)
Here's the timeline for these operations:
BEFORE UPDATE ON c:
decrement everything > OLD c.order_number (d--, e--)
increment everything >= NEW c.order_number (b++, d++, e++)
CURRENT STATE: a(0), b(2), c(2), d(3), e(4)
BEFORE UPDATE ON d:
decrement everything > OLD d.order_number (e--)
increment everything > NEW d.order_number (a++, b++, c++, e++)
CURRENT STATE: a(1), b(3), c(3), d(3), e(4)
SET c = 1
SET d = 0
FINAL STATE: d(0), a(1), c(1), b(3), e(4)
Clearly, the race condition here is the fact that c and d both alter each other's position in the list, but if the before operation trigger runs on each one before the state change happens, then the operations they perform on each other are discarded.
Is there a straightforward way to either make sure that locks are maintained on these tables through from start to finish of this operation, or otherwise to do this in a way that fixes this sort of race condition? I've been considering creating a separate table File_Structure_Lock that would be locked for update in a before trigger, and then unlocked in the after trigger to circumvent the PostgreSQL locking system, but I figured that there had to be a better method.
EDIT: I was asked for the actual SQL. My issue here is in preparation for a refactor on code that was already existing due to that code having race conditions that were causing errors. I can try to mark this up in a minute, but here's the raw code that I'm working with, with a few variable name changes to make it more generally understandable
CREATE OR REPLACE FUNCTION getOrderLock() RETURNS TRIGGER AS $getOrderLock$
BEGIN
PERFORM * FROM Folders FOR UPDATE;
PERFORM * FROM Files FOR UPDATE;
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
RETURN OLD;
END IF;
END;
$getOrderLock$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_folder_lock_rows
BEFORE INSERT OR UPDATE OR DELETE ON Folders
FOR EACH STATEMENT
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE getOrderLock();
CREATE TRIGGER trigger_file_lock_rows
BEFORE INSERT OR UPDATE OR DELETE ON Files
FOR EACH STATEMENT
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE getOrderLock();
CREATE OR REPLACE FUNCTION adjust_order_numbers_after_folder_update() RETURNS TRIGGER AS $adjust_order_numbers_after_nav_update$
BEGIN
--update old location
UPDATE Folders
SET order_number = Folders.order_number - 1
WHERE Folders.order_number >= OLD.order_number
AND Folders.page_id = OLD.page_id
AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0)
AND Folders.id != NEW.id;
UPDATE Files
SET order_number = Files.order_number - 1
WHERE Files.order_number >= OLD.order_number
AND Files.page_id = OLD.page_id
AND COALESCE(Files.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0);
--update new location
UPDATE Folders
SET order_number = Folders.order_number + 1
WHERE Folders.order_number >= NEW.order_number
AND Folders.page_id = NEW.page_id
AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0)
AND Folders.id != NEW.id;
UPDATE Files
SET order_number = Files.order_number + 1
WHERE Files.order_number >= NEW.order_number
AND Files.page_id = NEW.page_id
AND COALESCE(Files.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0);
RETURN NEW;
END;
$adjust_order_numbers_after_nav_update$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION adjust_order_numbers_after_file_update() RETURNS TRIGGER AS $adjust_order_numbers_after_file_update$
BEGIN
--update old location
UPDATE Folders
SET order_number = Folders.order_number - 1
WHERE Folders.order_number >= OLD.order_number
AND Folders.page_id = OLD.page_id
AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0);
UPDATE Files
SET order_number = Files.order_number - 1
WHERE Files.order_number >= OLD.order_number
AND Files.page_id = OLD.page_id
AND COALESCE(Files.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0)
AND Files.id != NEW.id;
--update new location
UPDATE Folders
SET order_number = Folders.order_number + 1
WHERE Folders.order_number >= NEW.order_number
AND Folders.page_id = NEW.page_id
AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0);
UPDATE Files
SET order_number = Files.order_number + 1
WHERE Files.order_number >= NEW.order_number
AND Files.page_id = NEW.page_id
AND COALESCE(Files.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0)
AND Files.id != NEW.id;
RETURN NEW;
END;
$adjust_order_numbers_after_file_update$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_folder_order_shift
AFTER UPDATE ON Folders
FOR EACH ROW
WHEN (
(
COALESCE(OLD.folder_parent_id, 0) != COALESCE(NEW.folder_parent_id, 0)
OR OLD.order_number != NEW.order_number
OR Old.page_id != New.page_id
)
AND pg_trigger_depth() < 1
)
EXECUTE PROCEDURE adjust_order_numbers_after_folder_update();
CREATE TRIGGER trigger_file_order_shift
AFTER UPDATE ON Files
FOR EACH ROW
WHEN (
(
COALESCE(OLD.folder_parent_id, 0) != COALESCE(NEW.folder_parent_id, 0)
OR OLD.order_number != NEW.order_number
OR Old.page_id != New.page_id
)
AND pg_trigger_depth() < 1
)
EXECUTE PROCEDURE adjust_order_numbers_after_file_update();

The problem seems to come from the order_number that you insist in being a gap-less sequence of integers ordering the items in each folder. If you want to maintain that, you have to shuffle all items around, and it is indeed hard to do that without some major locking.
But if all you want to do is to maintain a certain order of the items, I would relax the requirement of a gap-less sequence and instead use double precision values to describe the order of items. Then it is easy to insert an item anywhere without changing order_number in any other element – you can always assign the moved item an order_number that is between any two existing ones.

Related

Is the cursor variable updated when updating a row?

In the code below (stored procedure body), whether the value of the cursor field is automatically updated after UPDATE or not? If not, is the Close / Open command sufficient again or not?
I didn't find any description that included this, it was just the FOR SELECT cursors in all of them.
DECLARE VARIABLE FCU_VALIDATE TYPE OF COLUMN FCU_CTRL.FCU_VAL_WHEN_IMP;
DECLARE FCU_DOC_MSTR CURSOR FOR
(SELECT * FROM FCU_DOC_MSTR
WHERE FCU_DOC_APN = :APNUMBER
AND FCU_DOC_ID = :DOCID);
BEGIN
OPEN FCU_DOC_MSTR;
FETCH FIRST FROM FCU_DOC_MSTR;
-- CHECK CONTROL FILE SETTINGS
FCU_VALIDATE = COALESCE((SELECT FCU_VAL_WHEN_IMP FROM FCU_CTRL
WHERE FCU_INDEX1 = 1), FALSE);
IF (FCU_VALIDATE = TRUE) THEN
BEGIN
-- IF EXIST INVALID ITEM DETAIL LINE, SET DOCUMENT STATUS TO INVALID
IF ((SELECT COUNT(*) FROM FCU_ITEM_DET
WHERE FCU_ITEM_APN = :FCU_DOC_MSTR.FCU_DOC_APN
AND FCU_ITEM_DOC_ID = :FCU_DOC_MSTR.FCU_DOC_ID
AND FCU_ITEM_STATUS != '0') > 0) THEN
UPDATE FCU_DOC_MSTR
SET FCU_DOC_STATUS = '90'
WHERE CURRENT OF FCU_DOC_MSTR;
END
-- CHECK DOCUMENT STATUS IS IMPORTED AND NO ERROR EXIST SET STATUS TO IMPORTED
IF (FCU_DOC_MSTR.FCU_DOC_STATUS = '99') THEN
UPDATE FCU_DOC_MSTR
SET FCU_DOC_STATUS = '0'
WHERE CURRENT OF FCU_DOC_MSTR;
IF (FCU_VALIDATE = TRUE) THEN
BEGIN
IF (FCU_DOC_MSTR.FCU_DOC_STATUS = '0') THEN
UPDATE FCU_DOC_MSTR
SET FCU_DOC_STATUS = '1'
WHERE CURRENT OF FCU_DOC_MSTR;
-- UPDATE FILE STATUS
IF ((SELECT COUNT(*) FROM FCU_DOC_MSTR
WHERE FCU_DOC_FILE_ID = :FCU_DOC_MSTR.FCU_DOC_FILE_ID
AND FCU_DOC_STATUS != '1') > 0) THEN
UPDATE FCU_FILE_MSTR
SET FCU_FILE_STATUS = '90'
WHERE FCU_FILE_ID = :FCU_DOC_MSTR.FCU_DOC_FILE_ID;
ELSE
UPDATE FCU_FILE_MSTR
SET FCU_FILE_STATUS = '1'
WHERE FCU_FILE_ID = :FCU_DOC_MSTR.FCU_DOC_FILE_ID;
END
CLOSE FCU_DOC_MSTR;
END
If the update is done through the cursor (using UPDATE ... WHERE CURRENT OF _cursor_name_), then the cursor record variable for the current row is also updated.
See this fiddle for a demonstration.
This was not documented in the Firebird 3.0 Release Notes, but it was documented in the doc/sql.extensions/README.cursor_variables.txt included with your Firebird installation. This is also been documented in the Firebird 3.0 Language Reference, under FETCH:
Reading from a cursor variable returns the current field values. This
means that an UPDATE statement (with a WHERE CURRENT OF clause)
will update not only the table, but also the fields in the cursor
variable for subsequent reads. Executing a DELETE statement (with a
WHERE CURRENT OF clause) will set all fields in the cursor variable
to NULL for subsequent reads

How to write the for condition and if condition combination?writing the code **For loop**

I am writing the code For loop
my table have 2 columns main_bug_id,parent_id
main_bug_id parent_id
1
2 1
3 2
have to check the main is in the parent bug or not, if the main bug is not in parent bug the task status have to update
for(record)
if (record.agent_assignment_id = parent_id)then
return parent_id in var_id
else if (record.agent_assignment_id = var_id)then
return variable_id
else if
update project.nt_task
set status = 'Open'
where key_val_id = variable_id
end if;
end;

How can I write an iterative function that bases the current row output off of the prior row output?

I need to determine whether my current row value is positive or negative, which is a function of a starting value, scheduled increases, and daily decrement (which is different depending on if the prior day output value was positive or negative).
I only know my starting number for day 1, my schedule of increases, and my decrement values if positive or negative.
If "Prior Day Output" + "Today scheduled increase" is positive, then "Prior Day Output" + "Today scheduled increase" - 2(decrement value)
If "Prior Day Output" + "Today scheduled increase" is negative, then "Prior Day Output" + "Today scheduled increase" - 1(decrement value)
I haven't tried anything, as I can't think of an algebraic way to perform this. New to iterative functions or loops.
Here is the data I have to start with:
Here is what I want to end with:
I believe I have a solution for you.
Note: You have a stipulation saying if start_val is positive (>= 0) set the decrement to 2 but, on Day 11 of your output example you have the decrement set to 1 where start_val + increase = 0.
This solution will match your example output which considers 0 to be negative. That is easily changeable in the segment that sets new_dec. Just move the = to the appropriate location.
CREATE OR REPLACE FUNCTION update_vals()
RETURNS SETOF test
AS
$$
DECLARE
new_dec integer;
new_end integer;
new_start integer;
rec record;
BEGIN
FOR rec IN
SELECT * FROM test
LOOP
new_start := NULL::integer;
IF rec.start_val IS NULL
THEN
SELECT end_val
INTO new_start
FROM
(
SELECT MAX(id) last_id FROM test WHERE id < rec.id
) a
JOIN test ON id = a.last_id
;
END IF;
IF COALESCE(rec.start_val, new_start) + rec.increase > 0
THEN
new_dec := 2;
ELSIF COALESCE(rec.start_val, new_start) + rec.increase <= 0
THEN
new_dec := 1;
END IF;
new_end := COALESCE(rec.start_val, new_start) + rec.increase - new_dec;
IF new_start IS NOT NULL
THEN
RETURN QUERY
UPDATE test
SET (start_val, decrement, end_val) = (new_start, new_dec, new_end)
WHERE id = rec.id
RETURNING *
;
ELSE
RETURN QUERY
UPDATE test
SET (decrement, end_val) = (new_dec, new_end)
WHERE id = rec.id
RETURNING *
;
END IF;
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
Here is a db-fiddle to show a working example.

Postgresql, maintaining hierarchical data with triggers

I have adjacency list table account, with columns id, code, name, and parent_id.
To make sorting and displaying easier I added two more columns: depth, and path (materialized path). I know, postgresql has a dedicated data type for materialized path, but I'd like to use a more generic approach, not specific to postgresql. I also applied several rules to my design:
1) code can be up to 10 characters long
2) Max depth is 9; so root account can have sub accounts at maximum 8 level deep.
3) Once set, parent_id is never changed, so there's no need to move a branch of tree to another part of the tree.
4) path is an account's materialized path, which is up to 90 characters long; it is built by concatenating account codes, right padded to 10 characters long; for example, like '10000______10001______'.
So, to automatically maintain depth and path columns, I created a trigger and a trigger function for the account table:
CREATE FUNCTION public.fn_account_set_hierarchy()
RETURNS TRIGGER AS $$
DECLARE d INTEGER; p CHARACTER VARYING;
BEGIN
IF TG_OP = 'INSERT' THEN
IF NEW.parent_id IS NULL THEN
NEW.depth := 1;
NEW.path := rpad(NEW.code, 10);
ELSE
BEGIN
SELECT depth, path INTO d, p
FROM public.account
WHERE id = NEW.parent_id;
NEW.depth := d + 1;
NEW.path := p || rpad(NEW.code, 10);
END;
END IF;
ELSE
IF NEW.code IS DISTINCT FROM OLD.code THEN
UPDATE public.account
SET path = OVERLAY(path PLACING rpad(NEW.code, 10)
FROM (OLD.depth - 1) * 10 + 1 FOR 10)
WHERE SUBSTRING(path FROM (OLD.depth - 1) * 10 + 1 FOR 10) =
rpad(OLD.code, 10);
END IF;
END IF;
RETURN NEW;
END$$
LANGUAGE plpgsql
CREATE TRIGGER tg_account_set_hierarchy
BEFORE INSERT OR UPDATE ON public.account
FOR EACH ROW
EXECUTE PROCEDURE public.fn_account_set_hierarchy();
The above seems to work for INSERTs. But for UPDATEs, an error is thrown: "UPDATE statement on table 'account' expected to update 1 row(s); 0 were matched.". I have a doubt on "UPDATE public.account ..." part. Can someone help me correct the above trigger?
Well, in the above code, update part updates all records, including the record, on which the trigger was fired (concurrency execption?). That seems not to work. So I had to issue 2 different statements:
UPDATE {0}.{1} SET path = OVERLAY(path PLACING rpad(NEW.code, 10)
FROM (OLD.depth - 1) * 10 + 1 FOR 10)
WHERE SUBSTRING(path FROM (OLD.depth - 1) * 10 + 1 FOR 10) = rpad(OLD.code, 10)
AND id <> NEW.id;
NEW.path = OVERLAY(OLD.path PLACING rpad(NEW.code, 10)
FROM (OLD.depth - 1) * 10 + 1 FOR 10);

SQL SErver Trigger not evaluating as Insert or Update properly

I want to have one trigger to handle updates and inserts. Most of the sql actions in the trigger are for both. The only exception is the fields I'm using to record date and username for an insert and an update. This is what I have, but the updates of the fields used to track update and insert are not firing right. If I insert a new record, I get CreatedBy, CreatedOn, LastEditedBy, LastEditedOn populated, with LastEditedOn as 1 second after CreatedOn (which I dont want to happen). When I update the record, only the LastEditedBy & LastEditedOn changes (which is correct). I'm including my full trigger for reference:
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
-- =================================================================================
-- Author: Paul J. Scipione
-- Create date: 2/15/2012
-- Update date: 6/5/2012
-- Description: To concatenate several fields into a set formatted UnitDescription,
-- to total Span & Loop footages, to set appropriate AcctCode, & track
-- user inserts
-- =================================================================================
IF OBJECT_ID('ProcessCable', 'TR') IS NOT NULL
DROP TRIGGER ProcessCable
GO
CREATE TRIGGER ProcessCable
ON Cable
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- IF TRIGGER_NESTLEVEL() > 1 RETURN
IF ((SELECT TRIGGER_NESTLEVEL()) > 1 )
RETURN
ELSE
BEGIN
-- record user and date of insert or update
IF EXISTS (SELECT * FROM DELETED)
UPDATE Cable SET LastEditedOn = getdate(), LastEditedBy = REPLACE(user_name(), 'GRTINET\', '')
ELSE IF NOT EXISTS (SELECT * FROM DELETED)
UPDATE Cable SET CreatedOn = getdate(), CreatedBy = REPLACE(user_name(), 'GRTINET\', '')
-- reset Suffix if applicable
UPDATE Cable SET Suffix = NULL WHERE Suffix = 'n/a'
-- create UnitDescription value
UPDATE Cable SET UnitDescription =
isnull (Type, '') +
isnull (CONVERT (NVARCHAR (10), Size), '') +
'-' +
isnull (CONVERT (NVARCHAR (10), Gauge), '') +
CASE
WHEN ExtraTrench IS NOT NULL AND ExtraTrench > 0 THEN
CASE
WHEN Suffix IS NULL THEN 'TE' + '(' + CONVERT (NVARCHAR (10), ExtraTrench) + ')'
ELSE 'TE' + '(' + CONVERT (NVARCHAR (10), ExtraTrench) + ')' + Suffix
END
ELSE isnull (Suffix, '')
END
-- convert any accidental negative numbers entered
UPDATE Cable SET Length = ABS(Length)
-- sum Length with LoopFootage into TotalFootage
UPDATE Cable SET TotalFootage = isnull(Length, 0) + isnull(LoopFootage, 0)
-- set proper AcctCode based on Type
UPDATE Cable SET AcctCode =
CASE
WHEN Type IN ('SEA', 'CW', 'CJ') THEN '32.2421.2'
WHEN Type IN ('BFC', 'BJ', 'SEB') THEN '32.2423.2'
WHEN Type IN ('TIP','UF') THEN '32.2422.2'
WHEN Type = 'unknown' OR Type IS NULL THEN 'unknown'
END
WHERE AcctCode IS NULL OR AcctCode = ' '
END
END
GO
A few things jump out at me when I look at your trigger:
You are doing several additional updates rather than a single update (performance-wise, a single update would be better).
Your update statements are unconstrained (there is no JOIN to the inserted/deleted tables to limit the number of records that you perform these additional updates on).
Most of this logic feels like it should be in the application layer rather than in the database; Or, perhaps in some cases implemented differently.
Some quick examples:
Suffix of "n/a" should be removed before inserted.
Cable length absolute value should be done before inserted (with a CHECK CONSTRAINT to verify that bad data cannot be inserted).
TotalFootage should be a computed column so it is always correct.
The Type/AcctCode relationship seems like it should be a column value in a foreign key reference.
But ultimately, I think the reason you are seeing the unexpected dates is because of the unconstrained updates. Without addressing any of the other concerns I brought up above, the statement that sets the audit fields should be more like this:
UPDATE Cable SET LastEditedOn = getdate(), LastEditedBy = REPLACE(user_name(), 'GRTINET\', '')
FROM Cable
JOIN deleted on Cable.PrimaryKeyColumn = deleted.PrimaryKeyColumn
UPDATE Cable SET CreatedOn = getdate(), CreatedBy = REPLACE(user_name(), 'GRTINET\', '')
FROM Cable
JOIN inserted on Cable.PrimaryKeyColumn = inserted.PrimaryKeyColumn
LEFT JOIN deleted on Cable.PrimaryKeyColumn = deleted.PrimaryKeyColumn
WHERE deleted.PrimaryKeyColumn IS NULL