I am fairly new at writing procedures (beyond the basics)
I am trying to write a stored procedure that inserts into a table (dbo.billing_batch) based on a select statement that loops through the list of results (#DealerID FROM dbo.vehicle_info).
The SELECT DISTINCT... statement on its own works perfectly and returns a list of 54 records.
The result of the SELECT statement is dynamic and will change from week to week, so I cannot count on 54 records each time.
I am trying to use WHILE #DealerID IS NOT NULL to loop through the INSERT routine.
The loop is supposed to update dbo.billing_batch, however it is inserting the same 1st record (BillingBatchRosterID, DealerID) over and over and over to infinity.
I know I must be doing something wrong (I have never written a stored procedure that loops).
Any help would be greatly appreciated!
Here is the stored procedure code:
ALTER PROCEDURE [dbo].[sp_billing_batch_set]
#varBillingBatchRosterID int
AS
SET NOCOUNT ON;
BEGIN
DECLARE #DealerID int
SELECT DISTINCT #DealerID = vi.DealerID
FROM dbo.vehicle_info vi
LEFT JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4 OR vi.ItemStatusID = 5 OR vi.ItemStatusID = 8)
END
WHILE #DealerID IS NOT NULL
BEGIN TRY
INSERT INTO dbo.billing_batch (BillingBatchRosterID, DealerID)
VALUES(#varBillingBatchRosterID, -- BillingBatchRosterID - int
#DealerID) -- DealerID - int
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription
END CATCH
You have the same problems as another recent post here: Iterate over a table with a non-int id value
Why do a loop? Just do it as a single SQL statement
If you must use a loop, you will need to update your #Dealer value at each run (e.g., to the next DealerId) otherwise it will just infinitely loop with the same DealerID value
Don't do a loop.
Here's an example not needing a loop.
ALTER PROCEDURE [dbo].[P_billing_batch_set]
#varBillingBatchRosterID int
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
INSERT INTO dbo.billing_batch (DealerID, BillingBatchRosterID)
SELECT DISTINCT vi.DealerID, #varBillingBatchRosterID
FROM dbo.vehicle_info vi
INNER JOIN dbo.dealer_info di ON di.DealerID = vi.DealerID
WHERE di.DealerActive = 1
AND (vi.ItemStatusID < 4
OR vi.ItemStatusID = 5
OR vi.ItemStatusID = 8
);
END TRY
BEGIN CATCH
SELECT ' There was an error: ' + error_message() AS ErrorDescription;
END CATCH;
END;
Note I
Changed the LEFT JOIN to an INNER JOIN as your WHERE clause needs the record to exist in the dealer_info table
Moved the SET NOCOUNT ON; to be within the BEGIN-END section
Moved the END to the end
Renamed your stored procedure as per the excellent comment from #marc_s (on the question itself)
I'm trying to create the following trigger in SQL Server, but SSMS throws an error and I have no clue what it is. Any thoughts ?
Msg 156, Level 15, State 1, Line 2
Incorrect syntax near the keyword 'trigger'.
Code:
IF NOT EXISTS(SELECT * FROM sys.triggers
WHERE object_id = OBJECT_ID(N'[dbo].[trAfterUpdateInfoDoc]'))
CREATE TRIGGER [dbo].[trAfterUpdateInfoDoc]
ON [dbo].[InfoDocs]
AFTER UPDATE
AS
BEGIN
DECLARE #infodoctemplateid INT;
DECLARE #infodocid INT;
DECLARE #requireccount FLOAT(2);
DECLARE #filledcount FLOAT(2);
DECLARE #pcnt FLOAT(2);
DECLARE c CURSOR FOR
SELECT id
FROM InfoDocs ifd
WHERE exists (SELECT 1 FROM Inserted AS i WHERE i.id = ifd.id)
OPEN c
FETCH NEXT FROM c INTO #infodocid
WHILE ##Fetch_Status = 0
BEGIN
SELECT #infodoctemplateid = InfoDocTemplateId
FROM InfoDocs
WHERE id = #infodocid;
SELECT #requireccount = COUNT(*)
FROM InfoDocTemplateFields
WHERE InfoDocTemplateId = #infodoctemplateid
AND IsRequired = 1;
IF (#requireccount = 0)
BEGIN
set #pcnt = 100;
END
ELSE
BEGIN
select #filledcount = count(*) from InfoDocFields
where InfoDocId = #infodocid
and InfoDocTemplateFieldId in (select id from InfoDocTemplateFields where InfoDocTemplateId = #infodoctemplateid and IsRequired = 1)
and (BooleanValue is not null or (StringValue is not null and StringValue <> '') or IntValue is not null or DateValue is not null)
set #pcnt = #filledcount / #requireccount * 100.0;
END
update InfoDocs set PercentageCompleted = #pcnt Where id = #infodocid;
Fetch next From c into #infodocid
End
Close c
Deallocate c
END
Create Trigger (Limitations section) must be the first statement in a batch, so you can't use the IF exists check before it.
In SQL Server 2016 SP1 onwards, you can use CREATE OR ALTER TRIGGER... for the same behaviour.
Pre-SQL Server 2016 SP1, there's some suggestions here
I also second Zohar's comment that putting this logic into a trigger could well cause you many performance issues & possibly hard to track down unexpected behaviour/bugs.
Anytime a SQL object like a trigger is created, it needs to be the only object created in the batch. A batch is terminated by the keyword GO.
Try refactoring your code to fit this general structure and see if it works:
IF OBJECT_ID(N'[dbo].[trAfterUpdateInfoDoc]') IS NOT NULL
DROP TRIGGER [dbo].[trAfterUpdateInfoDoc]
GO
CREATE TRIGGER [dbo].[trAfterUpdateInfoDoc]
ON [dbo].[InfoDocs]
AFTER UPDATE
AS
BEGIN
--PLACE CODE HERE
END
GO
I am trying to write a stored procedure that reads a column in a particular row of a table, then updates that column with a new value. The orig. is returned.
I want it to lock the row from others till I am done. What is the process?
I have something like
CREATE PROCEDURE [dbo].[aptc_Prt_NextDocumentNumberGet]
(#_iFormatConfigID INT, #_oNextDocumentNumber FLOAT OUTPUT)
AS
BEGIN
DECLARE #FrameworkConfig XML
SET #_oNextDocumentNumber = - 1
DECLARE #NewNextDocumentID FLOAT
SELECT
#_oNextDocumentNumber = FrameworkConfig.value('(/Parameters/Parameter[#Name="NextDocNo.NextDocumentNumber"])[1]', 'float')
FROM
[ttcPrtFormatConfig] WITH (ROWLOCK)
WHERE
FormatConfigID = #_iFormatConfigID
-- Select the Next Doc num out of the xml field
-- increment appropriate control and set output
IF #_iFormatConfigID IS NOT NULL
BEGIN
-- set what will be the "next" doc number after we add this current txn
IF (ABS(#_oNextDocumentNumber - 99999999999999999) < 0.0001)
BEGIN
SELECT #NewNextDocumentID = 1
END
ELSE
BEGIN
SELECT #NewNextDocumentID = #_oNextDocumentNumber + 1
END
UPDATE [ttcPrtFormatConfig]
WITH (ROWLOCK)
SET FrameworkConfig.modify('
replace value of
(/Parameters/Parameter[#Name="NextDocNo.NextDocumentNumber"]/text())[1]
with sql:variable("#NewNextDocumentID")')
WHERE FormatConfigID = #_iFormatConfigID
END
END
This should get you close to what you want.
DECLARE #MyValue INT
--You need a transaction so that the scope of your lock is well defined
BEGIN TRANSACTION
BEGIN TRY
--Get the value you are interested in, This select will lock the row so other people will not even be able to read it until you are finished!!!!!
SELECT #MyValue = MyValue
FROM MyTable WITH (UPDLOCK HOLDLOCK)
WHERE MyValue = SomeValue
--Do your checks and updates. You can take as long as you like as you are the only person who can do a read or update of this data.
IF
BEGIN
UPDATE MyTable
END
--Make sure you commit or rollback! this will release the lock
END TRY
BEGIN CATCH
--Oh no bad stuff! give up and put it back to how it was
PRINT ERROR_MESSAGE() + N' Your message here'
--Check there is a transaction that we can rollback
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK;
END
--You may want to return some error state and not throw!
THROW;
--RETURN -1 --(for example)
END CATCH;
--yay it all worked and your lock will be released
COMMIT
--Do what you like with the old value
RETURN #MyValue
I need to return a resultset consisting of database errors from a SQL Server stored procedure's CATCH clause but I'm stuck with it. Do I need to use cursors to return resultset and if so, then what is the type declaration for the OUTPUT parameter in my .NET application? I tried Object and Variant but did not work.
I also tried the simple way just using a SELECT statement to return and it works with one stored procedure but not with another as thus in my CATCH clause:
while (#I <= #count)
begin
BEGIN TRY
-- delete all previous rows inserted in #customerRow for previous counts #I
delete from #customerRow
-- this is inserting the current row that we want to save in database
insert into #customerRow
SELECT
[id],[firstname], [lastname], [street], [city],
[phone],[mobile],[fax], [email], [companyName],
[licence],[brn], [vat], [companyStreet], [companyCity], [status]
FROM
(SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS rownumber,
[id], [firstname], [lastname], [street], [city],
[phone], [mobile], [fax], [email], [companyName],
[licence], [brn], [vat], [companyStreet], [companyCity], [status]
FROM
#registerDetails) AS foo
WHERE
rownumber = #I
-- this stored procedure handles the saving of the current customer row just defined above
-- if there is any error from that sproc, it will jump to CATCH block
--save the error message in the temp table and continue
--with the next customer row in the while loop.
exec dbo.sp_SaveCustomer #customerRow
END TRY
BEGIN CATCH
IF ##TranCount = 0
-- Transaction started in procedure.
-- Roll back complete transaction.
ROLLBACK TRANSACTION;
if XACT_STATE()= -1 rollback transaction
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE() + ' ' + (select firstname from #registerDetails where id=#I)
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE()
INSERT INTO #registrationResults (error,id)
SELECT #ErrorMessage, #I
END CATCH
set #I = #I +1
end
COMMIT TRANSACTION registerTran
select * from #registrationResults
The above works with one stored procedure when I call it in my vb.net code as :
ta.Fill(registrationErrors, clientDetailsDT)
where registrationErrors and clientDetailsDT are strongly typed data tables.
This one does not :
begin catch
IF ##TranCount > 0 or XACT_STATE()= -1 ROLLBACK TRANSACTION;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
DECLARE #ErrorLine INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
SELECT #ErrorLine = ERROR_Line();
****ERROR -- THIS SELECT WAS MESSING ALL UP as it was this select that was being returned to the .NET and not the select of the desired #temp table after, hence returning 0 resultset as this select was EMPTY. !!
select status_indicator from InsertInvoiceTriggerData where session_GUID = guid**
delete from InsertInvoiceTriggerData where session_GUID = #guid**
INSERT INTO #registrationResults (error,id)
SELECT #ErrorMessage, NULL
select * from #registrationResults
end catch
Any suggestions how to return resultsets?
I haven't seen your database code, but in my experience the very first error caught by catch means that the entire transaction has to be rolled back. Apart from other things, it also implies that I never have more than 1 error to return in any given situation.
As such, I use 2 scalar output parameters in my stored procedures, that is:
#Error int = null output,
#Message nvarchar(2048) = null output
And I can retrieve them just like any other output variables.
UPD: Even after you have added some code, I still fail to understand what is your problem, exactly. However, I see several problems with your code, so I'll point them out and chances are, one of them will solve the problem. I am commenting only the first snippet, since the last one is too incomplete.
You should have been started the outermost transaction somewhere before the loop. If not, the code will fail.
If I guessed correctly, you implemented all savepoint logic inside the dbo.sp_SaveCustomer stored proc. If not, the whole discussion is pointless, since there are no save tran or rollback #savepoint statements in the code you have shown.
The first catch statement - IF ##TranCount = 0 ROLLBACK TRANSACTION is all wrong. If the condition is successful, it will result in error trying to rollback nonexistent transaction. Should not be here if you rely on savepoints.
The next after it should result in unconditional break:
if XACT_STATE()= -1 begin
rollback transaction;
break;
end;
The rest of your catch code can be replaced with this:
INSERT INTO #registrationResults (error, id)
SELECT error_message() + ' ' + firstname, id
from #registerDetails where id=#I;
Also, never use temp tables for this purpose, because rollback will affect them as well. Always use table variables for this, they are non-transactional (just like any other variable).
The commit should be conditional, because you may end up at this point with no transaction to commit:
if ##trancount > 0 commit tran;
There is no point in specifying savepoint name in the commit statement, it only leads to confusion (though isn't considered an error). Also, there should not any savepoint in this module (unless you have defined it before the loop).
I suspect that's just the tip of the iceberg, since I have no idea what actually happens inside the dbo.SaveCustomer stored procedure.
UPD2: Here is a sample of my VB.NET code which I use to receive recordsets from stored procedures:
Private Function SearchObjectsBase( _
SearchMode As penum_SEARCH_MODE, SearchCriteria As String
) As DataSet
Dim Cmd As DbCommand, Pr As DbParameter, dda As DbDataAdapter
' Initialise returning dataset object
SearchObjectsBase = New DataSet()
Cmd = MyBase.CreateCommand(String.Format("dbo.{0}", SearchMode))
With Cmd
' Parameter definitions
Pr = .CreateParameter()
With Pr
.ParameterName = "#SearchCriteria"
.DbType = DbType.Xml
.Value = SearchCriteria
End With
.Parameters.Add(Pr)
' Create data adapter to use its Fill() method
dda = DbProviderFactories.GetFactory(.Connection).CreateDataAdapter()
' Assign the prepared DbCommand as a select method for the adapter
dda.SelectCommand = Cmd
' A single resultset is expected here
dda.Fill(SearchObjectsBase)
End With
' Set error vars and get rid of it
Call MyBase.SetErrorOutput(Cmd)
' Check for errors and, if any, discard the dataset
If MyBase.ErrorNumber <> 0 Then SearchObjectsBase.Clear()
End Function
I use .NET 4.5, which has a very nice method to automatically select the most appropriate data adapter based on the actual connection.
And here is a call of this function:
Dim XDoc As New XElement("Criteria"), DS As DataSet = Nothing, DT As DataTable
...
DS = .SearchPatients(XDoc.ToString(SaveOptions.None))
' Assign datasource to a grid
Me.dgr_Search.DataSource = DS.Tables.Item(0)
Here, SearchPatients() is a wrapper on top of the SearchObjectsBase().
We have a type of DDL construct that sometimes (but not always) fails. The construct is:
IF NOT EXISTS (SELECT 'b'
FROM sys.COLUMNS A
INNER JOIN sys.TABLES B
ON A.OBJECT_ID = B.OBJECT_ID
WHERE a.NAME = 'BadgeID'
AND b.NAME = 'AllianceIncentBadges')
BEGIN
ALTER TABLE dbo.ALLIANCEINCENTBADGES
ADD BADGEID INT NOT NULL DEFAULT 0;
EXEC Sp_executesql
N'MERGE INTO AllianceIncentBadges USING IncentiveBadges ON AllianceIncentBadges.BadgeName = IncentiveBadges.BadgeName WHEN MATCHED THEN UPDATE SET BadgeID = IncentiveBadges.BadgeID;'
;
END;
go
If I run the 'ALTER' Table first and then the 'EXEC', all is fine. However, if I run it using the above construct, the BadgeID field is not added and the EXEC throws an error for the missing column. I use the 'EXEC' so that the parser doesn't throw an error (the parser will throw an error if the column doesn't exist at parse time).
Any ideas why this problem crops up from time to time?
BEGIN TRY
ALTER TABLE dbo.AllianceIncentBadges ADD BadgeID int NOT NULL Default 0;
MERGE INTO AllianceIncentBadges USING IncentiveBadges ON AllianceIncentBadges.BadgeName = IncentiveBadges.BadgeName WHEN MATCHED THEN UPDATE SET BadgeID = IncentiveBadges.BadgeID;
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> '2705' THROW;
END CATCH