T-SQL String Replace is Not Locking - Last Update Wins - sql

I have the following stored procedure, which is intended to iterate through a list of strings, which contains several substrings of the form prefix.bucketName. I want to iterate through each string and each bucket name, and replace the old prefix with a new prefix, but keep the same bucket name.
To give an example, consider this original string:
"(OldPrefix.BucketA)(OldPrefix.BucketB)"
So for example I would like to get:
"(NewPrefix.BucketA)(NewPrefix.BucketB)"
What I actually get is this:
"(OldPrefix.BucketA)(NewPrefix.BucketB)"
So, in general, only one of the prefixes get updated, and it is not predictable which one. Based on some investigation I have done, it appears that both replacements actually work, but only the last one is actually saved. It seems like SQL should be locking this column but instead, both are read at the same time, the replace is applied, and then both are written, leaving the last write as what shows in the column.
Here is the query - All variable names have been changed for privacy - Some error handling and data validation code was left out for brevity:
DECLARE #PrefixID INT = 1478,
DECLARE #PrefixName_OLD NVARCHAR(50) = 'OldPrefix',
DECLARE #PrefixName_NEW NVARCHAR(50) = 'NewPrefix'
BEGIN TRAN
-- Code to rename the section itself here not shown for brevity
UPDATE
dbo.Component
SET
AString= REPLACE(AString,'('+#Prefix_OLD+'.'+b.BucketName+')', '('+#PrefixName_NEW+'.'+b.BucketName+')'),
FROM
dbo.Component sc
JOIN
dbo.ComponentBucketFilterInString fis
ON
sc.ComponentID = fis.ComponentID
JOIN
dbo.Buckets b
ON
fis.BucketID = b.BucketID
WHERE
b.PrefixID = #PrefixID
COMMIT
RETURN 1
When I write the same query using a while loop, it performs as expected:
DECLARE #BucketsToUpdate TABLE
(
BucketID INT,
BucketName VARCHAR(256)
)
INSERT INTO #BucketsToUpdate
SELECT BucketID, BucketName
FROM Buckets WHERE PrefixID = #PrefixID
WHILE EXISTS(SELECT 1 FROM #BucketsToUpdate)
BEGIN
DECLARE #currentBucketID INT,
#currentBucketName VARCHAR(256)
SELECT TOP 1 #currentBucketID = bucketID, #currentBucketName = bucketName FROM #BucketsToUpdate
UPDATE
dbo.Component
SET
AString = REPLACE(AString,'('+#PrefixName_OLD+'.'+#currentBucketName+')', '('+#PrefixName_NEW+'.'+#currentBucketName+')')
FROM
dbo.Component sc
JOIN
dbo.ComponentBucketFilterInString fis
ON
sc.ComponentID = fis.ComponentID
WHERE fis.BucketID = #currentBucketID
DELETE FROM #BucketsToUpdate WHERE BucketID = #currentBucketID
END
Why does the first version fail? How can I fix it?

The problem you are experiencing is "undefined" behavior when there is more than single match possible for UPDATE FROM JOIN.
In order to make your update possible you should run it multiple times updating one pair of values at a time as you proposed in your second code demo.
Related: How is this script updating table when using LEFT JOINs? and Let’s deprecate UPDATE FROM!:
SQL Server will happily update the same row over and over again if it matches more than one row in the joined table, >>with only the result of the last of those updates sticking<<.

Not sure why you are making the whole process so complex. May be I am not clearly understanding the requirement. As per my understanding, you are looking to update only Prefix part for column 'AString' in the table dbo.Component. Current value for example is-
(OldPrefix.BucketA)(OldPrefix.BucketB)
You wants to update the value as-
(NewPrefix.BucketA)(NewPrefix.BucketB)
Am I right? If yes, you can update all records with a simple Update script as below-
DECLARE #PrefixID INT = 1478
DECLARE #PrefixName_OLD NVARCHAR(50) = 'OldPrefix'
DECLARE #PrefixName_NEW NVARCHAR(50) = 'NewPrefix'
UPDATE Component
SET AString= REPLACE(AString,#PrefixName_OLD,#PrefixName_NEW)

Related

update SQL rows with multidimensional array as source

Goal:
I want to update existing entries in my SQL table [dbo.properties]. The SQL command is executed in PHP. The PHP file again receives an array, as new data source. This array contains a unique ID, propertyName and the actual value.
Problem:
How do I loop, in SQL, through an array and make sure to update the values at the correct point?
My SQL table look like:
[id] as [int] increments by DB
[property] as [varchar(50)]
[value] as [varchar(50)]
The passed array look like:
0: Object {id:'30', property:'sugar', value:'20g'}
1: Object {id:'37', property:'salt', value:'10g'}
2: Object {id:'38', property:'chocolate', value:'120g'}
I know how to do it with single data or to delete multiple values with a list. But I its tough to find anything similar for my case. Especually as I need to update all in one query and the amount of rows is dynamic. Means it could be that only one item is updated or 10.
Pseudo SQL query for better understand
BEGIN TRANSACTION [updateProperties]
BEGIN TRY
UPDATE properties SET
// Somehow iterate through array
property = ('array[Pos][Entry1]'),
value = ('array[Pos][Entry2]')
WHERE id = ('array[Pos][Entry0]')
COMMIT TRANSACTION [updateProperties]
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION [updateProperties]
END CATCH;
If you pass this in as a proper JSON array, you can use OPENJSON to join to your table
DECLARE #json nvarchar(max) = N'[
{"id":30, "property":"sugar", "value":"20g"},
{"id":37, "property":"salt", "value":"10g"},
{"id":38, "property":"chocolate", "value":"120g"}
]';
UPDATE p
SET property = j.property,
value = j.value
FROM properties p
JOIN OPENJSON(#json)
WITH (
id int,
[property] varchar(50),
[value] varchar(50)
) j ON j.id = p.id;
You can use MERGE if you want to update existing and insert new rows.
On a side note, I would advise you not to store value as a varchar. Instead, split it into amount and unit.

SQL Server Trigger Variable scope

Could you please tell me what's wrong this this trigger (below is just a portion of the trigger).
When put variable on where parameter I don't get any result, instead, when I set is a default value I get correct result
Like this it doesn't work (I verified and #Cont_local is not null and data really exists inside the database):
Note:
info (information) is in a different table but on the same database.
the trigger is set to work on Cont (Contact) Table
select #Cont_local = ContID from inserted
SET #Addr = (select Addr from Info I where I.ContID = #Cont_local)
But like this, it works
select #Cont_local = '454877'
SET #Addr = (select Addr from Info I where I.ContID = #Cont_local)
While #Cont_local is returning some values, the select statement cannot detect it.
Is this related to the scope of #Cont_local variable or something else?
Instead, when I try to put data manually, query return values and store it on #Addr
Here is Complete code
ALTER TRIGGER [dbo].[Insert__]
ON [dbo].[Cont]
AFTER INSERT
AS
BEGIN
DECLARE #Cont_local nvarchar(50);
DECLARE #Name nvarchar(50);
DECLARE #firstnameme nvarchar(50);
DECLARE #Addr nvarchar(50);
select #Name = contName from inserted
select #firstname = contfirstname from inserted
select #Cont_local = ContID from inserted
SET #Addr = (select Addr from Info I where I.ContID = #Cont_local)
INSERT INTO Final_Contact (Name, firstname, address) values
(#Name, #firstname ,#Addr)
END
select #Cont_local = ContID from inserted
This line of code works only when in the inserted-table is only one record
This addresses the original version of the question.
I don't see why your code would return an error. However, it is malformed and an error waiting to happen. Let me explain.
This code:
select #Cont_local = ContID from inserted
Assumes that inserted has one row. Of course, the code runs when inserted has more than one row. It just returns one value.
The inserted and deleted views in SQL Server triggers can refer to multiple rows. In other words, triggers are set-based processing rather than row-by-row processing. In a properly structured trigger, the references should always be in FROM clauses.
The following example is totally made up, but it illustrates how to update a column in another table using information from inserted:
update a
set addr = c.addr
where inserted i join
info i
on i.contid = a.contid join
addresses a
on a.addrid = i.addrid

SQL Server 2014 : FileTable trigger on update File_Stream.PathName()

I have a FileTable FT and another table AT. In AT, I have extended metadata properties of the file in FT.
I have tried to create a trigger ON FT FOR UPDATE that will update the file path that is in AT.
Here is what I've tried:
ALTER TRIGGER [dbo].[u_filepath]
ON [FileDB].[dbo].[FT]
FOR UPDATE
AS
Declare #newpath nvarchar(1000)
Declare #oldpath nvarchar(1000)
select #newpath = file_stream.pathname()
from inserted
select #oldpath = file_stream.pathname()
from deleted
update AT
set [Path] = #newpath
where [Path] = #oldpath
GO
When I execute the query, it spins. I'm planning on leaving it running overnight just in case it decides to do something.
I want the Path column in AT to update to the updated file_stream.PathName() from FT.
Is the trigger logical?
Should I store the file_stream BLOB in my AT Path column instead of the actual path?
Your trigger has MAJOR flaw in that you seem to assume it'll be called once per row - that is not the case.
The trigger will fire once per statement, so if your UPDATE statement that causes this trigger to fire inserts 25 rows, you'll get the trigger fired once, but then the Deleted and Inserted pseudo tables will each contain 25 rows.
Which of those 25 rows will your code select here?
select #newpath = file_stream.pathname()
from inserted
It's non-deterministic, you'll get one arbitrary row and you will be ignoring all other rows.
You need to rewrite your trigger to take this into account - use a set-based approach to updating your data - not a row-based one - something like this:
ALTER TRIGGER [dbo].[u_filepath]
ON [FileDB].[dbo].[FT]
FOR UPDATE
AS
-- create a CTE to get the primary key, old and new path names
WITH UpdatedPaths AS
(
SELECT
i.PrimaryKey,
OldPathName = d.file_stream.pathname(),
NewPathName = i.file_stream.pathname()
FROM
Inserted i
INNER JOIN
Deleted d ON i.PrimaryKey = d.PrimaryKey
)
-- update the "AT" table where the old and new path names don't match
UPDATE dbo.AT
SET [Path] = up.NewPathName
FROM UpdatedPaths up
WHERE
up.OldPathName <> up.NewPathName
dbo.AT.PrimaryKey = up.PrimaryKey
GO

SQL Trigger to update row

I need a SQL trigger that would zero pad a cell whenever its inserted or updated. Was curious if its best practice to append two strings together like I'm doing in the update command. Is this be best way to do it?
CREATE TRIGGER PadColumnTenCharsInserted ON Table
AFTER INSERT
AS
DECLARE
#pad_characters VARCHAR(10),
#target_column NVARCHAR(255)
SET #pad_characters = '0000000000'
SET #target_column = 'IndexField1'
IF UPDATE(IndexField1)
BEGIN
UPDATE Table
SET IndexField1 = RIGHT(#pad_characters + IndexField1, 10)
END
GO
Your padding code looks fine.
Instead of updating every row in the table like this:
UPDATE Table
update just the row that triggered the trigger:
UPDATE updated
Also, you've still got some extraneous code -- everything involving #target_column. And it looks like you're not sure if this is an INSERT trigger or an UPDATE trigger. I see AFTER INSERT and IF UPDATE.
Two questions:
What are you doing with #target_column? You declare it and set it with a column name, but then you never use it. If you intend to use the variable in your subsequent SQL statements, you may need to wrap the statements in an EXECUTE() or use sp_executesql().
The syntax "UPDATE Table..." is OK for your update statement assuming that "Table" is the name of the table you are updating. What seems to be missing is a filter of some kind. Or did you really intend for that column to be updated for every row in the whole table?
One way to handle this would be to declare another variable and set it with the PK of the row that is updated, then use a where clause to limit the update to just that row. Something like this:
DECLARE #id int
SELECT #id = Record_ID FROM INSERTED
-- body of your trigger here
WHERE Record_ID = #id
I like your padding code. It looks good to me.

SQL Table Locking

I have an SQL Server locking question regarding an application we have in house. The application takes submissions of data and persists them into an SQL Server table. Each submission is also assigned a special catalog number (unrelated to the identity field in the table) which is a sequential alpha numeric number. These numbers are pulled from another table and are not generated at run time. So the steps are
Insert Data into Submission Table
Grab next Unassigned Catalog
Number from Catalog Table
Assign the Catalog Number to the
Submission in the Submission table
All these steps happen sequentially in the same stored procedure.
Its, rate but sometimes we manage to get two submission at the same second and they both get assigned the same Catalog Number which causes a localized version of the Apocalypse in our company for a small while.
What can we do to limit the over assignment of the catalog numbers?
When getting your next catalog number, use row locking to protect the time between you finding it and marking it as in use, e.g.:
set transaction isolation level REPEATABLE READ
begin transaction
select top 1 #catalog_number = catalog_number
from catalog_numbers with (updlock,rowlock)
where assigned = 0
update catalog_numbers set assigned = 1 where catalog_number = :catalog_number
commit transaction
You could use an identity field to produce the catalog numbers, that way you can safely create and get the number:
insert into Catalog () values ()
set #CatalogNumber = scope_identity()
The scope_identity function will return the id of the last record created in the same session, so separate sessions can create records at the same time and still end up with the correct id.
If you can't use an identity field to create the catalog numbers, you have to use a transaction to make sure that you can determine the next number and create it without another session accessing the table.
I like araqnid's response. You could also use an insert trigger on the submission table to accomplish this. The trigger would be in the scope of the insert, and you would effectively embed the logic to assign the catalog_number in the trigger. Just wanted to put your options up here.
Here's the easy solution. No race condition. No blocking from a restrictive transaction isolation level. Probably won't work in SQL dialects other than T-SQL, though.
I assume their is some outside force at work to keep your catalog number table populated with unassigned catalog numbers.
This technique should work for you: just do the same sort of "interlocked update" that retrieves a value, something like:
update top 1 CatalogNumber
set in_use = 1 ,
#newCatalogNumber = catalog_number
from CatalogNumber
where in_use = 0
Anyway, the following stored procedure just just ticks up a number on each execution and hands back the previous one. If you want fancier value, add a computed column that applies the transform of choice to the incrementing value to get the desired value.
drop table dbo.PrimaryKeyGenerator
go
create table dbo.PrimaryKeyGenerator
(
id varchar(100) not null ,
current_value int not null default(1) ,
constraint PrimaryKeyGenerator_PK primary key clustered ( id ) ,
)
go
drop procedure dbo.GetNewPrimaryKey
go
create procedure dbo.GetNewPrimaryKey
#name varchar(100)
as
set nocount on
set ansi_nulls on
set concat_null_yields_null on
set xact_abort on
declare
#uniqueValue int
--
-- put the supplied key in canonical form
--
set #name = ltrim(rtrim(lower(#name)))
--
-- if the name isn't already defined in the table, define it.
--
insert dbo.PrimaryKeyGenerator ( id )
select id = #name
where not exists ( select *
from dbo.PrimaryKeyGenerator pkg
where pkg.id = #name
)
--
-- now, an interlocked update to get the current value and increment the table
--
update PrimaryKeyGenerator
set #uniqueValue = current_value ,
current_value = current_value + 1
where id = #name
--
-- return the new unique value to the caller
--
return #uniqueValue
go
To use it:
declare #pk int
exec #pk = dbo.GetNewPrimaryKey 'foobar'
select #pk
Trivial to mod it to return a result set or return the value via an OUTPUT parameter.