Global temporary table scope behaves differently inside a stored procedure - sql-server

The following code executes successfully, unless I add create procedure dbo.proc_name ... as to it:
use db_name
go
declare #sp_date date;
select #sp_date = getdate();
if object_id('tempdb..##global_tmp_tbl') is not null drop table ##global_tmp_tbl;
begin transaction
set xact_abort on
declare #query varchar(250), #exec_stmnt varchar(500);
set #query = 'exec remote_db.dbo.remote_sp' + ' ''''' + cast(#sp_date as varchar(10)) + ''''' ';
set #query = '''' + #query + '''';
set #exec_stmnt = 'select * into ##global_tmp_tbl from openquery(LS_RMT,' + #query + ')';
exec (#exec_stmnt);
commit transaction
go
if object_id('tempdb..#local_tmp_tbl') is not null drop table #local_tmp_tbl;
select * into #local_tmp_tbl from ##global_tmp_tbl;
Here LS_RMT is a linked server, and remote_sp is a stored procedure on the database remote_db on that linked server.
When I try to put this code into a stored procedure, SQL Server complains that ##global_tmp_tbl is an invalid name when trying to read from it after executing the stored procedure on the linked server which loads it.
I'm guessing that the scope of the global temporary table changes once within the context of a stored procedure, but I can't find any documentation on why that might be the case so I'm not sure.
Is this a scope issue, or is it actually possible to use the global temporary table within a stored procedure after it has been created inside a transaction that loads it from an openquery statement and I am just doing it wrong?

Although I am not entirely sure why the code above works when outside the context of a stored procedure, I did determine that embedding all references to the global temporary table within the committed transaction allowed the stored procedure to work. So something like the following:
use db_name
go
create procedure dbo.proc_name
#sp_date date = NULL
as
if isnull(#sp_date,'') = ''
begin
select #sp_date = getdate();
end
if object_id('tempdb..##global_tmp_tbl') is not null drop table ##global_tmp_tbl;
begin transaction
set xact_abort on
declare #query varchar(250), #exec_stmnt varchar(500);
set #query = 'exec remote_db.dbo.remote_sp' + ' ''''' + cast(#sp_date as varchar(10)) + ''''' ';
set #query = '''' + #query + '''';
set #exec_stmnt = 'select * into ##global_tmp_tbl from openquery(LS_RMT,' + #query + ')';
exec (#exec_stmnt);
if object_id('tempdb..#local_tmp_tbl') is not null drop table #local_tmp_tbl;
select * into #local_tmp_tbl from ##global_tmp_tbl;
commit transaction
go

Related

SQL Server stored procedure error: "Error Message: Must declare the scalar variable" referencing a variable with sp_executesql but not without

When I want to add datetime column to entire table I can write a stored procedure which takes a date as input and then stores it into the table - like so:
ALTER PROCEDURE [dbo].[set_datettime]
(#importDate VARCHAR(100))
AS
BEGIN
UPDATE [dbo].[table1]
SET uploadDate = #importDate
END
But when I want to make the table dynamic I need to use sp_executesql. So my thought is I can do this:
ALTER PROCEDURE [dbo].[set_datettime]
(#tableName VARCHAR(100), #importDate VARCHAR(100))
AS
BEGIN
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = #importDate';
EXECUTE sp_executesql #Sql
END
but now I get an error:
Error Message: Must declare the scalar variable #importDate
Despite clearly declaring the variable. Even if I try to explicitly declare the variable again I get the error that I cant declare duplicate variables.
Other thing which I tried was to do:
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = ' + #importDate;
But this throws an error
Invalid column name 10-10-2019
Lastly I was able to accomplish the task (somewhat) by changing to
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = GETDATE()';
But in this solution I define the date in the stored procedure and doesn't take it as input, which is not ideal.
How can I have dynamic table definition while still keeping the date input variable dynamic also?
You need to parametrise your dynamic statement. I'm typing on my phone right now, so I apologise for any typographical errors:
EXEC sp_executesql #SQL, N'#importdate date', #importdate;
Never inject parameters in your dynamic statements. It creates huge security flaws in your SQL, called SQL Injection.
Edit: not on my phone now, so can write out the complete SP:
ALTER PROCEDURE [dbo].[set_datettime]
(#tableName sysname, #importDate date)
AS
BEGIN
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = #importDate;';
EXECUTE sp_executesql #Sql, N'#importDate date', #importDate;
END;
Test it first:
declare #tableName varchar(100) = 'sometable'
, #importDate varchar(100) = '10-10-2019'
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = ' + #importDate
select #Sql
This give you an incorrect statement:
UPDATE dbo.[sometable] SET uploadDate = 10-10-2019
Try this:
SET #Sql = 'UPDATE dbo.' + quotename(#tableName) + ' SET uploadDate = ' + '''' + #importDate + ''''
Which gives you
UPDATE dbo.[sometable] SET uploadDate = '10-10-2019'

Need help. Message says temp table doesn't exist: 'Invalid object name '#TempCodes'.'

If I run the select statement below by itself (with the table name hard coded in) it runs fine and the temp table is created. If I run it as below it says 'Invalid object name '#TempCodes'' although when I print #Sql1 it looks exactly the same as when I run it by itself (with the table name hard coded in).
Any ideas on what's going on here will be appreciated.
DECLARE #TableName NVARCHAR(50)
SET #TableName = '[My Table Name]'
DECLARE #Sql1 NVARCHAR(MAX);
SET #Sql1 = N'SELECT AccountNumber,LTRIM(RTRIM(m.n.value(''.[1]'',''varchar(8000)''))) AS mdcodes INTO #TempCodes FROM (SELECT AccountNumber,CAST(''<XMLRoot><RowData>''
+ REPLACE(MD_Results,'','',''</RowData><RowData>'')
+ ''</RowData></XMLRoot>'' AS XML) AS x FROM ' + #TableName
+ N')t CROSS APPLY x.nodes(''/XMLRoot/RowData'')m(n)'
IF OBJECT_ID('tempdb.dbo.#TempCodes', 'U') IS NOT NULL
BEGIN
drop table #TempCodes
END
EXECUTE sp_executesql #Sql1
Select * from #TempCodes
I believe ## is a typo. Even if you fix it to # it will throw same error.
EXECUTE sp_executesql #Sql1
A local temporary table created in a stored procedure is dropped
automatically when the stored procedure is finished.
In your case sp_executesql is the stored procedure.
the table created inside the dynamic query will be dropped after the exec sp_executesql is completed. That's why you are getting that error.
You need to create table outside and use Insert into table..select syntax inside the dynamic query
IF OBJECT_ID('tempdb.dbo.#TempCodes', 'U') IS NOT NULL
drop table #TempCodes
create table #TempCodes(AccountNumber varchar(100),mdcodes varchar(100))
SET #Sql1 = N'Insert into #TempCodes
SELECT AccountNumber,LTRIM(RTRIM(m.n.value(''.
[1]'',''varchar(8000)''))) AS mdcodes FROM (SELECT
AccountNumber,CAST(''<XMLRoot><RowData>'' +
REPLACE(MD_Results,'','',''</RowData><RowData>'') + ''</RowData></XMLRoot>''
AS XML) AS x FROM '+#TableName+ N')t CROSS APPLY
x.nodes(''/XMLRoot/RowData'')m(n)'
Try using a global temp table ##TempCodes as local temporary tables are only visible in current session.
DECLARE #TableName NVARCHAR(50)
SET #TableName = '[My Table Name]'
DECLARE #Sql1 NVARCHAR(MAX);
SET #Sql1 = N'SELECT AccountNumber,LTRIM(RTRIM(m.n.value(''.
[1]'',''varchar(8000)''))) AS mdcodes INTO ##TempCodes FROM (SELECT
AccountNumber,CAST(''<XMLRoot><RowData>'' +
REPLACE(MD_Results,'','',''</RowData><RowData>'') + ''</RowData></XMLRoot>''
AS XML) AS x FROM '+#TableName+ N')t CROSS APPLY
x.nodes(''/XMLRoot/RowData'')m(n)'
IF OBJECT_ID('tempdb.dbo.##TempCodes', 'U') IS NOT NULL
BEGIN
drop table ##TempCodes
END
EXECUTE sp_executesql #Sql1
Select * from ##TempCodes

Inserting data into a temp table from an open query

I've looked online and you can not drop variables into an open query so easily so I modified my query and it says the #TempOT temp table is "Invalid object name '#TempOT'." What am I doing wrong that is causing the temp table not to get populated?
BEGIN
IF OBJECT_ID('tempdb..#TempOT') IS NOT NULL
BEGIN
DROP TABLE #TempOT
END;
DECLARE #TSQL As varchar(1024)
DECLARE #AS400Query As varchar(1024)
SET #AS400Query = 'SELECT * from blah.blahlibrary.AS400Table where DATETS BETWEEN '+ #BegofMonthForAS400 + ' AND ' + #DateForAS400
SET #TSQL = 'SELECT * FROM OPENQUERY(ISERIES,' + '''' + #AS400Query +'''' + ')'
INSERT INTO #TempOT
EXEC(#TSQL)
END
i have also tried this and it does not work
BEGIN
IF OBJECT_ID('tempdb..#TempOT') IS NOT NULL
BEGIN
DROP TABLE #TempOT
END;
DECLARE #TSQL As varchar(1024)
DECLARE #AS400Query As varchar(1024)
SET #AS400Query = 'SELECT * from valdosta.PRDATA.PRTIMELM where DATETS BETWEEN '+ #BegofMonthForAS400 + ' AND ' + #DateForAS400
SET #TSQL = ' INSERT INTO #TempOT SELECT * FROM OPENQUERY(ISERIES,' + '''' + #AS400Query +'''' + ')'
EXEC(#TSQL)
END
You DROP the TEMP table and then you are trying to INSERT data into it.
You need to create the TEMP table within your script first.
The problem is in fact, that you are creating temporary table within EXEC statement. Created table #TempOT is dropped immediately after batch is executed` and is not visible for your main query. There are at least two possibilities (if you want to use temp tables):
Instead of creating local temporary table, create global temporary table with ## before table name (difference between them is explained here). In my opinion, you should drop this table at the end of your main query.
Create #TempOt with all columns before EXEC statement where you will be able without problem to populate table #TempOt with desired data.

SQL - define a variable that points to a stored procedure

I have several queries that drop the proc if it exists, recreate it, and set permissions on it, similar to this:
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO
if exists (select *
from dbo.sysobjects
where id = object_id(N'[dbo].[spMyStoredProcedureName]')
and OBJECTPROPERTY(id, N'IsProcedure') = 1)
drop procedure [dbo].[spMyStoredProcedureName]
GO
CREATE PROCEDURE [dbo].[spMyStoredProcedureName]
AS
/* more proc stuff */
GRANT EXECUTE ON [dbo].[spMyStoredProcedureName]
TO Some_User_Group
My question is: is there some way to define a variable for [dbo].[spMyStoredProcedureName] so that I can declare it once and refer to the variable? I need to use the variable two ways - once as a string in the select statement and the rest of the time as a reference to the stored proc I'm creating/dropping.
I'm assuming your goal is to replace your "several scripts" with just one script that can be used by changing the value of the variable that holds the name of the stored procedure.
If so you could try a dynamic approach:
Declare #spMyStoredProcedureName nvarchar(255);
--either use a cursor or whatever means to populate the variable.
--for this example I will simply set it
SET #spMyStoredProcedureName = '[dbo].[spMyStoredProcedureName]';
DECLARE #sql nvarchar(max);
SET #sql='
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS ON
GO
if exists (select *
from dbo.sysobjects
where id = object_id(N''#spMyStoredProcedureName'')
and OBJECTPROPERTY(id, N'IsProcedure') = 1)
drop procedure #spMyStoredProcedureName
GO
CREATE PROCEDURE #spMyStoredProcedureName
AS
/* more proc stuff */
GRANT EXECUTE ON #spMyStoredProcedureName
TO Some_User_Group
';
SET #sql = REPLACE(#sql, '#spMyStoredProcedureName', #spMyStoredProcedureName)
EXECUTE(#sql)
Note that you'll need to escape all the single quotes in your stored proc code when doing this. Also I'm not so sure you can use the GO command in dynamic sql. If not, you'd need to do two separate dynamic statements per stored proc.
Use dynamic sql for your intended ddl statement. A sample implementation below.
-- Declare variable to store the procedure name
Declare #proc varchar(100) = 'myProc';
-- Construct the ddl statement
DECLARE #script nvarChar(MAX);
SET #script =
'
if exists (select *
from dbo.sysobjects
where id = object_id(N'''+ #proc + ''')
and OBJECTPROPERTY(id, N''IsProcedure'') = 1)
drop procedure ' + #proc + ';
GO
CREATE PROCEDURE ' + #proc + '
AS
print ''Hello World!'';
/* more proc stuff */
GO
GRANT EXECUTE ON ' + #proc + '
TO Some_User_Group;
'
SET #script = 'EXEC (''' + REPLACE(REPLACE(#script, '''', ''''''),
'GO', '''); EXEC(''') + ''');'
-- Run the dynamic ddl
EXEC (#script);
-- Test
exec myproc;

GRANTing permissions across different databases (schemas)

I'm securing the DB by only allowing interaction with the DB through a series of Sprocs; pretty common fare.
I've dug up and modified a script which loops through and assigns the user EXECUTE permission for all non-system SProcs. It works a treat except that I'd ideally like to add it to the Master DB so that I can easily use it for any subsequent projects. Yes, I could save simple as a .sql file but I'd prefer it this way.
The problem is that I don't know how to dynamically refer to objects in another DB. For example, I can easily query on MyDB.dbo.INFORMATION_SCHEMA.ROUTINES, but if the DB name is dynamic (e.g. #MyDBName), how can I query the objects in this DB?
Edit: Thanks to the posters below, I now have a working solution:
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[spGrantExec]
#User sysname,
#DB varchar(50),
#Target varchar(50)
AS
/*---------------------------- SQL 2005 + -------------------------------*/
SET NOCOUNT ON
-- 1 - Variable declarations
DECLARE #SQL varchar(8000)
-- 2 - Create temporary table
Set #SQL =
'USE #DB
DECLARE #MAXOID int
DECLARE #OwnerName varchar(128)
DECLARE #ObjectName varchar(128)
DECLARE #CMD1 varchar(8000)
CREATE TABLE #StoredProcedures
(OID int IDENTITY (1,1),
StoredProcOwner varchar(128) NOT NULL,
StoredProcName varchar(128) NOT NULL)
-- 3 - Populate temporary table
INSERT INTO #StoredProcedures (StoredProcOwner, StoredProcName)
SELECT ROUTINE_SCHEMA, ROUTINE_NAME
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_NAME LIKE ''' + #Target + '%''
AND ROUTINE_TYPE = ''PROCEDURE''
-- 4 - Capture the #MAXOID value
SELECT #MAXOID = MAX(OID) FROM #StoredProcedures
-- 5 - WHILE loop
WHILE #MAXOID > 0
BEGIN
-- 6 - Initialize the variables
SELECT #OwnerName = StoredProcOwner,
#ObjectName = StoredProcName
FROM #StoredProcedures
WHERE OID = #MAXOID
-- 7 - Build the string
SELECT #CMD1 = ''GRANT EXEC ON '' + ''['' + #OwnerName + '']'' + ''.'' + ''['' + #ObjectName + '']'' + '' TO #user''
-- 8 - Execute the string
Print #CMD1
EXEC(#CMD1)
-- 9 - Decrement #MAXOID
SET #MAXOID = #MAXOID - 1
END
-- 10 - Drop the temporary table
DROP TABLE #StoredProcedures'
Set #SQL = REPLACE(REPLACE(REPLACE(#SQL, '#DB', #DB), '#User', #User), '#Target', #Target)
--Select #SQL
--Print #SQL
Exec (#SQL)
SET NOCOUNT OFF
Similiar to #Cade's answer, the way to do this is to use dynamic sql. Before each call to a database table, add '#DbName.' Then replace the #DbName with the actual database name (the database name can't be passed as a variable in SQL, so you have to do the replace).
Also Cursors are normally considered evil for performance reasons, however using one in this case makes sense. For one, it would greatly simplify the procedure, plus since you're only going to run this once during application updates, you probably won't notice a performance hit, even if it added an extra second or two (which I doubt it would add anywhere near that much).
ALTER PROCEDURE [dbo].[spGrantExec]
#User SysName,
#DbName VarChar(512)
AS
BEGIN
DECLARE #Sql VarChar(1024)
SET #Sql = 'DECLARE #OwnerName varchar(128)
DECLARE #ObjectName varchar(128)
DECLARE #Cmd1 VarChar(128)
DECLARE ProcCursor CURSOR FOR
SELECT ROUTINE SCHEMA, ROUTINE NAME
FROM #DbName.INFORMATION SCHEMA.ROUTINES
WHERE ROUTINENAME NOT LIKE ''dt %'' AND ROUTINE TYPE = ''PROCEDURE''
OPEN ProcCursor
FETCH NEXT FROM ProcCursor INTO #OwnerName, #ObjectName
WHILE ##FETCH STATUS = 0
BEGIN
SET #CMD1 = ''GRANT EXEC ON '' + ''['' + #OwnerName + '']'' + ''.'' + ''['' + #ObjectName + '']'' + '' TO '' + ''#user''
EXEC (#CMD1)
FETCH NEXT FROM ProcCursor INTO #OwnerName, #ObjectName
END
CLOSE ProcCursor
DEALLOCATE ProcCursor
'
SET #Sql = Replace(Replace(#Sql, '#DbName', #DbName), '#user', #User)
EXEC (#Sql)
END
You can call this using: EXEC [spGrantExec] 'bob', 'Northwind'
Sorry the spacing is a little off in the sp. Developed using Sql 2005.
I found another technique, which I think is cleaner:
SELECT #sql = 'CREATE VIEW ...'
SELECT #sp_executesql = quotename(#dbname) + '..sp_executesql'
EXEC #sp_executesql #sql
This relies on setting the database context by calling sp_executesql in the other database (just like one could call an SP in any database).
In your case it would be equivalent to:
SELECT #sp_executesql = quotename(#dbname) + '..sp_executesql'
EXEC #sp_executesql #CMD1
You can use the double exec technique.
In your case, instead of just:
EXEC(#CMD1)
You would have:
SET #CMD1 =
'USE OtherDatabase;
EXEC (''' + REPLACE(#CMD1, '''', '''''') + ''')'
EXEC(#CMD1)

Resources