Is the cursor variable updated when updating a row? - firebird

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

Related

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

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.

Firebird dynamic Var New and Old

I need validate dynamic Fields from a Table. For example:
CREATE TRIGGER BU_TPROYECTOS FOR TPROYECTOS
BEFORE UPDATE AS
DECLARE VARIABLE vCAMPO VARCHAR(64);
BEGIN
/*In then table "TCAMPOS" are the fields to validate*/
for Select CAMPO from TCAMPOS where TABLA = TPROYECTOS and ACTUALIZA = 'V' into :vCAMPO do
Begin
if (New.:vCAMPO <> Old.:vCampo) then
/*How i get dynamic New.Field1, New.Field2 on query return*/
End;
END ;
The question is : How can I put "The name of the field that the query returns me " in the above code .
Ie if the query returns me the field1 and field5 , I would put the trigger
if ( New.Field1 < > Old.Field1 ) or ( New.Field5 < > Old.Field5 ) then
There is no such feature in Firebird. You will need to create (and preferably) generate triggers that will reference all fields hard coded. If the underlying table changes or the requirements for validation, you will need to recreate the trigger to take the added or removed fields into account.

Cannot insert NULL value into column error

I have a issue where I want to update a column in a table and with a trigger to update same column but in another table. It says I cannot insert NULL but I can't seem to understand from where it gets that NULL value. This is the trigger:
CREATE TRIGGER Custom_WF_Update_WF_DefinitionSteps_DefinitionId ON WF.Definition
AFTER UPDATE AS BEGIN
IF UPDATE(DefinitionId)
IF TRIGGER_NESTLEVEL() < 2
BEGIN
ALTER TABLE WF.DefinitionSteps NOCHECK CONSTRAINT ALL
UPDATE WF.DefinitionSteps
SET DefinitionId =
(SELECT i.DefinitionId
FROM inserted i,
deleted d
WHERE WF.DefinitionSteps.DefinitionId = d.DefinitionId
AND i.oldPkCol = d.DefinitionId)
WHERE WF.DefinitionSteps.DefinitionId IN
(SELECT DefinitionId FROM deleted)
ALTER TABLE WF.DefinitionSteps CHECK CONSTRAINT ALL
END
END
This update statement works just fine:
UPDATE [CCHMergeIntermediate].[WF].[Definition]
SET DefinitionId = source.DefinitionId + 445
FROM [CCHMergeIntermediate].[WF].[Definition] source
But this one fails:
UPDATE [CCHMergeIntermediate].[WF].[Definition]
SET DefinitionId = target.DefinitionId
FROM [CCHMergeIntermediate].[WF].[Definition] source
INNER JOIN [centralq3].[WF].[Definition] target
ON (((source.Name = target.Name) OR (source.Name IS NULL AND target.Name IS NULL)))
I get the following error:
Msg 515, Level 16, State 2, Procedure Custom_WF_Update_WF_DefinitionSteps_DefinitionId, Line 7
Cannot insert the value NULL into column 'DefinitionId', table 'CCHMergeIntermediate.WF.DefinitionSteps'; column does not allow nulls. UPDATE fails.
If I do a select instead of the update statement, like this:
SELECT source.DefinitionId, target.DefinitionId
FROM [CCHMergeIntermediate].[WF].[Definition] source
INNER JOIN [centralq3].[WF].[Definition] target
ON (((source.Name = target.Name) OR (source.Name IS NULL AND target.Name IS NULL)))
I get this result:
http://i.stack.imgur.com/3cZsM.png (sorry for external link, I don't have enaugh reputation to post image here )
What am I doing wrong? What I don't see? What am I missing..?
The problem was in the trigger at the condition. I modified the second where from i.oldPkCol = d.DefinitionId to i.oldPkCol = **d.oldPkCol** and it worked.
UPDATE WF.DefinitionSteps
SET DefinitionId =
(SELECT i.DefinitionId
FROM inserted i,
deleted d
WHERE WF.DefinitionSteps.DefinitionId = d.DefinitionId
AND i.oldPkCol = **d.oldPkCol**)
WHERE WF.DefinitionSteps.DefinitionId IN
(SELECT DefinitionId FROM deleted)

Check the Column Is NULL or NOT in SQL SERVER

I have a procedure in SQL Server 2008 R2, I want to enter the data to vol_Hours column and check it before if it is not null then plus the entry with old data that it in the column, if it's NULL then add the entry to the column without plus the NULL value.
I cannot add 2+NULL because it's = NULL.
MY Code Is:
create procedure updateVolunteerHours
#vol_ID int, #vol_Hours int
As
if vol_Hours is NULL
-- vol_Hours it is the Column Name
Update Personal_Information set vol_Hours = #vol_Hours where vol_ID = #vol_ID
else
Update Personal_Information set vol_Hours = #vol_Hours + vol_Hours where vol_ID = #vol_ID
In this case, just update the adding expression to use COALESCE (or ISNULL or CASE) and remove the IF statement entirely.
Update Personal_Information
set vol_Hours = COALESCE(vol_Hours, 0) + #vol_Hours
where vol_ID = #vol_ID
If both branches did entirely different things, then the conditional would have to be altered to use the results of a query.
IF EXISTS(SELECT * FROM Personal_Information
WHERE vol_ID = #vol_ID
AND vol_Hours IS NULL) ..
.. but that's just not needed here.

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