Working stored procedure fails when called from SQL Agent Job - stored-procedures

I have a stored procedure that runs fine with no errors inside SQL Server Management Studio. However, when the same stored procedure is executed as a step of a SQL Agent Job, it terminates with:
Error 3930: The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
The stored procedure lives in a schema named [Billing]. Most of the tables it uses are also in the [Billing] schema.
The main stored procedure begins a database transaction. The stored procedures called by the main stored procedure do all of their work on that inherited transaction. The main stored procedure is responsible for committing or rolling back the transaction.
The database User running the SQL Agent Job Step is not in the Sysadmin role, nor is it dbo. It belongs to the db_datareader and db_datawriter database roles, and has been given Delete, Execute, Insert, References, Select, and Update, permissions in the [Billing] schema.
Here is the main stored procedure:
CREATE PROCEDURE [Billing].[GenerateBillXml]
#pUseProductionPsSystem bit
,#pPeriodYear int
,#pPeriodMonthNumber int
,#pDocumentTypeName varchar(20)
,#pUser varchar(100)
,#pFilePath varchar(500)
,#pExportDateTime datetime2(7)
,#pResultCode int = 0 OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET #pResultCode = 0;
DECLARE #transactionBegun bit = 0
,#RC int = 0
,#processDateTimeUtc datetime2(7) = GETUTCDATE()
,#billGenerationId int
,#PsJobSuffix char(2)
,#periodDate date
,#periodDateString char(6)
,#PsBaseJobNumber char(6)
,#okayToRun int = 0
,#msg varchar(500);
BEGIN TRY
/* calculate the period date */
SET #periodDate = CONVERT(date,CAST(#pPeriodMonthNumber as varchar) + '/01/' + CAST(#pPeriodYear as varchar))
SET #periodDateString = CONVERT(varchar, #periodDate, 12); -- yyMMdd
/* retrieve the job suffix */
SELECT #PsJobSuffix = CAST([PsJobSuffix] as char(2))
FROM [dbo].[DocumentType] dt
WHERE [Name] like #pDocumentTypeName;
/* build the base job number */
SET #PsBaseJobNumber = LEFT(#periodDateString, 4) + #PsJobSuffix
/*
* We've made it past the input check - record the fact that we're generating a bill
*/
INSERT [Billing].[BillGeneration] (
[PsBaseJobNumber], [Status], [RunBy], [ProcessDateTimeUtc]
) VALUES (
#PsBaseJobNumber, 'Running', #pUser, #processDateTimeUtc
);
IF ##ROWCOUNT = 1
SET #billGenerationId = SCOPE_IDENTITY();
EXECUTE #RC = [Billing].[_0_OkayToGenerateBill]
#PsBaseJobNumber
,#okayToRun OUTPUT
,#pResultCode OUTPUT;
IF #pResultCode = 0
BEGIN
-- called stored procedure completed without error
IF #okayToRun = -1 -- this bill has already been generated
BEGIN
SET #msg = 'The billing for job ' + CAST(#PsBaseJobNumber as varchar) + ' has already been produced.';
RAISERROR(#msg, 16, 1)
END
IF #okayToRun = -2 -- too early to run billing for this period
BEGIN
SET #msg = 'It is too early to generate billing for job ' + CAST(#PsBaseJobNumber as varchar) + '.';
RAISERROR(#msg, 16, 1)
END
IF #okayToRun <> 1 -- unknown error...
BEGIN
SET #msg = 'Unknown error occured while determining whether okay to generate bill for job ' + CAST(#PsBaseJobNumber as varchar) + '.';
RAISERROR(#msg, 16, 1)
END
END
ELSE
BEGIN
SET #msg = 'Unknown failure in sub-stored procedure [Billing].[_0_OkayToRun]() for job ' + CAST(#PsBaseJobNumber as varchar) + '.';
RAISERROR(#msg, 16, 1) -- will cause branch to CATCH
END
/* Okay to generate bill */
/* If not in a transaction, begin one */
IF ##TRANCOUNT = 0
BEGIN
BEGIN TRANSACTION
SET #transactionBegun = 1;
END
EXECUTE #RC = [Billing].[_1_GeneratePsPreBillData]
#PsBaseJobNumber
,#pUser
,#pResultCode OUTPUT;
IF #pResultCode = 0
BEGIN
-- stored proced ran to successful completion
EXECUTE #RC = [Billing].[_2_GetBillingDataForXmlGeneration]
#pUseProductionPsSystem
,#PsBaseJobNumber
,#pResultCode OUTPUT;
IF #pResultCode = 0
BEGIN
-- stored proced ran to successful completion
IF #transactionBegun = 1
-- all table data has been created/updated
COMMIT TRANSACTION
-- Output XML bill to file
EXECUTE #RC = [Billing].[_3_GenerateBillingXmlFilesForPsJob]
#PsBaseJobNumber
,#pFilePath
,#pExportDateTime
,#pResultCode OUTPUT;
IF #pResultCode <> 0
BEGIN
-- called stored procedure failed
SET #msg = '[Billing].[_3_GenerateBillingXmlFilesForPsJob]() failed for job ' + CAST(#PsBaseJobNumber as varchar);
RAISERROR(#msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET #msg = '[Billing].[_2_GetBillingDataForXmlGeneration]() failed for job ' + CAST(#PsBaseJobNumber as varchar);
RAISERROR(#msg, 16, 1) -- will cause branch to CATCH
END
END
ELSE
BEGIN
-- called stored procedure failed
SET #msg = '[Billing].[_1_GeneratePsPreBillData]() failed for job ' + CAST(#PsBaseJobNumber as varchar);
RAISERROR(#msg, 16, 1) -- will cause branch to CATCH
END
-- bill generation was successful
IF #billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Successful', [ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = #billGenerationId;
END TRY
BEGIN CATCH
-- rollback transaction if we started one
IF #transactionBegun = 1
ROLLBACK TRANSACTION
-- record the error
INSERT [Billing].[BillGenerationError] (
[DateTime], [Object], [ErrorNumber], [ErrorMessage]
) VALUES (
GETDATE(), OBJECT_NAME(##PROCID), ERROR_NUMBER(), ERROR_MESSAGE()
);
-- bill generation failed
IF #billGenerationId IS NOT NULL
UPDATE [Billing].[BillGeneration]
SET [Status] = 'Failed'
,[Note] = ERROR_MESSAGE()
,[ProcessEndDateTimeUtc] = GETUTCDATE()
WHERE [Id] = #billGenerationId;
SELECT ERROR_NUMBER() as ErrorNumber;
SELECT ERROR_MESSAGE() as ErrorMessage;
SET #pResultCode = 1
END CATCH
END

As #Lukasz Szozda hinted in one of his comments to my question, the issue was that when the SQL Agent job executed the BCP.EXE command, it was running under the service account used by SQL Agent, which for me is the fairly restrictive "Local System" account. At this point it became obvious to me that a Proxy account had to be used. So I created a Proxy under Operating System (CmdExec), which was the only choice that made sense.
I went back to the job step to change it to use the Proxy, but then noticed that in its current type of Transact-SQL script (TSQL), there is no way to assign a Proxy account.
After trying a few things, finally decided to put the TSQL statements that were in the job step into a new stored procedure, and then call that stored procedure from the SQL command-line executable SQLCMD.EXE. I then changed the job step type from Transact-SQL script (TSQL) to Operating System (CmdExec). I could then set the Run As field to the Proxy I created earlier. I specified the command to run as CMD.EXE /c SQLCMD.EXE -S [ServerName] -Q "EXEC [NewProcedureName] [parameters]".
If you're curious as to why I'm running SQLCMD.EXE under CMD.EXE, it's because one of the parameters to the new stored procedure was the current date in a particular format ('%date:~4,10%'), which the SQL Server job execution environment didn't support, but which CMD.EXE certainly does.
Overall, I think this took a bit more effort than I expected.

Related

How to initiate tables lock for stored procedure in Azure Synapse Analytics?

Currently, I encountered an issue on Azure Synapse Analytics. I have a parent_cust_industry table which is full refresh - The table loads using stored procedure as below:
CREATE PROCEDURE [test].[test_proc] AS
BEGIN
-- LOAD TYPE: Full refresh
IF EXISTS (SELECT 1 FROM sys.tables WHERE SCHEMA_NAME(schema_id) = 'test' AND name = 'test_ld' )
BEGIN
DROP TABLE [test].[test_ld]
END
ELSE IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE SCHEMA_NAME(schema_id) = 'test' AND name = 'test_ld' )
BEGIN
CREATE TABLE [test].[test_ld]
WITH
(
DISTRIBUTION = REPLICATE
, CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT CAST(src.[test_code] as varchar(5)) as [test_code],
CAST(NULLIF(src.[test_period], '') as varchar(5)) as [test_period],
CAST(NULLIF(src.[test_id], '') as varchar(8)) as [test_id]
FROM [test].[test_temp] as src
END
IF NOT EXISTS ( SELECT 1 FROM sys.tables WHERE SCHEMA_NAME(schema_id) = 'test' AND name = 'test_hd' )
BEGIN
RENAME OBJECT [test].[test] TO [test_hd]
END
IF NOT EXISTS ( SELECT 1 FROM sys.tables WHERE SCHEMA_NAME(schema_id) = 'test' AND name = 'test' )
BEGIN
RENAME OBJECT [test].[test_ld] TO [test]
END
IF EXISTS ( SELECT 1 FROM sys.tables WHERE SCHEMA_NAME(schema_id) = 'test' AND name = 'test_hd' )
BEGIN
DROP TABLE [test].[test_hd]
END
END
;
The error happens when there is another stored procedure runs at the same time to load data to another table and it requires the [test].[test] table which cause invalid object for [test].[test].
Normally, the [test].[test_proc] would finish the data load first before other store procs depend on it. But in rare occasion, the data is considerably large, it took more time to process and can cause the invalid object error.
Is there a locking mechanism that I can apply to the stored procedure [test].[test_proc] so that if the two store procs happen to run at the same time, the [test].[test_proc] would finish first then the remaining store procedure can start reading the data from [test].[test] table ?
As you do not have access to traditional SQL Server locking procs like sp_getapplock and the default transaction isolation level of Azure Synapse Analytics, dedicated SQL pools is READ UNCOMMITTED you have limited choices.
You could route all access to this proc through a single Synapse Pipeline and set its concurrency setting to 1. This would ensure only one pipeline execution could happen at once, causing subsequent calls to the same pipeline to queue up.
Set the pipeline concurrency in the Pipeline settings here:
So you could have a single main pipeline that routes to others, eg using the Switch or If activities and ensure the proc cannot be called by other pipelines - should work.

Handle dynamic parameter when null in the stored procedure

I working on a stored procedure in redshift. I see that when parameters passed are NULL to the Execute statement in stored procedure. It fails with cannot execute a null string.
Please give me insights on how to solve the problem.
Stored Procedure:
CREATE OR REPLACE PROCEDURE outer_proc() LANGUAGE plpgsql
AS $$
DECLARE
cond_holder RECORD;
iter RECORD;
BEGIN
drop table if exists tmp_direction_comms;
create temporary table tmp_direction_comms as select distinct code from direction_coms;
DROP TABLE if exists final_direction_comms;
EXECUTE 'CREATE TEMP TABLE final_direction_comms
(
code varchar(100),
direction varchar(100),
dir_flg Boolean
)';
FOR iter IN select code from tmp_direction_comms LOOP
RAISE INFO 'code is %', iter.code;
SELECT INTO cond_holder distinct condition FROM mapping where code = iter.code;
RAISE INFO 'engmnt_cd is %', cond_holder.condition;
EXECUTE 'INSERT INTO final_direction_comms select code, direction, case when NVL('||cond_holder.condition||',false) then true else false end as dir_flg
from direction_coms where code = '''||iter.code||'''';
END LOOP;
END;
$$;
EXECUTE 'INSERT INTO final_direction_comms select code, direction,
case when NVL('||cond_holder.condition||',false) then true else false end as dir_flg
from acp_edw.stg_edw.direction_coms where code = '''||iter.code||'''';
There are two variables that can be NULL - iter.code or cond_holder.condition. The cond_holder.condition is wrapped by NVL, but NVL is inside in result string, not in generating expression.
Second big issue is a vulnerability against SQL injection. Probably you should to do:
EXECUTE 'INSERT INTO final_direction_comms select code, direction,
case when ' || NVL(cond_holder.condition, false) ' || then true else false end as dir_flg
from acp_edw.stg_edw.direction_coms where code = $1'
USING iter.code;
I am not sure if Redshift has support for USING clause. If not, then you should to use quote_literal function:
'... where code = ' || quote_literal(iter.code);

SQLCODE=-104, SQLSTATE=42601, SQLERRMC=select Con_Gruop_Name from;t vparam = grpName; ;<delete>

I ve checked this "SQLCODE=-104, SQLSTATE=42601" this Error code but still not able to find what wrong with above proc.
I also execute the query and it worked fine. the below error I got when I run proc.
** SQLCODE=-104, SQLSTATE=42601, SQLERRMC=select Con_Gruop_Name from;t vparam = grpName; ;**
create OR REPLACE PROCEDURE getConGroup(in grpName varchar(100))
begin
declare vparam varchar(100);
set vparam = grpName;
select Con_Gruop_Name from Grp_Table where Gruop_Name=vparam;
end
1) verify Con_Gruop_Name and Gruop_Name is correct name, i suppose its Con_Group_Name and Group_Name
2) You can use parameter directly into your query
3) You must use cursor for return result select, like this
4) May be you should be add Library into your select " ... from yourlib.yourtable where ..."
CREATE PROCEDURE getConGroup (IN grpName varchar(100))
RESULT SETS 1
LANGUAGE SQL
P1: BEGIN
DECLARE cursor1 CURSOR WITH RETURN FOR
select Con_Gruop_Name from Grp_Table where Gruop_Name=grpName ;
OPEN cursor1;
END P1

Clean MySQL tables with failed foreign checks

I have a giant mysql sql dump file. But I'm getting error when I try to import it because of foreign key checks. Somehow there is missing data, so I'm importing it with
SET SESSION FOREIGN_KEY_CHECKS=0;
and it works, but I'm looking for a solution for missing data.
So is there any automatic way to find and delete relation data with missing entries to get a clean database dump, or I have to go and write manuel SQL for every relation, write query to delete missing values ?
You can automate a delete statement like this:
DELIMITER $$
DROP PROCEDURE IF EXISTS check_foreign $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `check_foreign`()
BEGIN
DECLARE finishing INTEGER DEFAULT 0;
DECLARE vstmt VARCHAR(4000);
DECLARE vtbname VARCHAR(50);
DECLARE vtbnameref VARCHAR(50);
DECLARE vtbcol VARCHAR(50);
DECLARE vtbcolref VARCHAR(50);
DECLARE cr_tables CURSOR FOR select a.table_name, a.referenced_table_name, a.column_name, a.referenced_column_name from information_schema.KEY_COLUMN_USAGE a where a.table_schema = 'protocol_manager' and a.REFERENCED_TABLE_NAME is not null order by a.table_name;
DECLARE CONTINUE HANDLER FOR not found SET finishing = 1;
OPEN cr_tables;
SET vstmt = '';
table_foreign_delete: loop
if finishing = 1 then
leave table_foreign_delete;
end if;
FETCH cr_tables INTO vtbname, vtbnameref, vtbcol, vtbcolref;
SET vstmt = CONCAT(vstmt, char(10), 'DELETE FROM ', vtbname, ' a WHERE NOT EXISTS (SELECT 1 FROM ', vtbnameref, ' b WHERE a.', vtbcol, ' = b.', vtbcolref, ');');
end loop table_foreign_delete;
select vstmt;
END$$
DELIMITER ;
You can even do deep search to find a way to execute it dynamicly. For example a temporary table with a trigger. You generate a delete statement, insert it into the temp table, trigger the insert that fires a another (func, proc) to execute the statement generated.

Sybase ASE - INSERT INTO statment i stored procedure, problems in formating string

I have following problem formatting the #p_f_field variable correct, I get the error:
Could not execute statement.
Incorrect syntax near ‘+’
Sybase error code=102, SQLState=”42000”
Severity Level=15, State=181, Transaction State=1
Line 7
My stored procedure:
create proc dbo.sp_m_efi_dw_full_dsa_table_load_im ( -- parameter examples:
#p_skm_name varchar(4096),
#p_usr_name varchar(4096),
#p_t_name_dsa varchar(4096),
#p_t_name_bas varchar(4096),
#p_f_name varchar(4096),
#p_f_field varchar(4096)
)
as begin
declare #sql varchar(4096)
if #p_skm_name is not null
begin
if #p_usr_name is not null
begin
set #sql='TRUNCATE TABLE '+#p_skm_name+'.'+#p_usr_name+'.'+#p_t_name_dsa
exec (#sql)
end
end
set #sql='INSERT INTO '+#p_skm_name+'.'+#p_usr_name+'.'+#p_t_name_dsa+' '+#p_f_name+
' VALUES '+#p_f_field
exec (#sql)
end
My call to the stored procedure:
execute BAS_efi.dbo.sp_m_efi_dw_full_dsa_table_load_im
#p_skm_name = 'B_ef',
#p_usr_name = 'dbo',
#p_t_name_dsa = 'a_log',
#p_t_name_bas = 'a_log',
#p_f_name = '(NEWFIELD1)',
#p_f_field = '('+char(39)+'daf9af01-6bc2-11e-b23182b0623e'+char(39)+')'
Any suggestions on how to format the variable #p_f_field correct, or any others suggestions on how to execute this simple INSERT INTO procedure?

Resources