I have a stored procedure that I want to use but I need to pass the table name into it.
ALTER PROCEDURE [dbo].[STT_Card_Entry_Temp_Write]
#CARD_NO as VarChar(20),
#tablename as table
AS
BEGIN
Update live.scheme.sttakedm
set adjustment_quantit= (TM.expected_quantity-TM.counted)*-1,
take_sign=case left((TM.expected_quantity-TM.counted),1)
when '-' then '+'
else '-'
end,
status=''
from live.scheme.sttakedm (nolock) stt
inner join #tablename TM (NOLOCK)
on TM.card_number=#CARD_NO
and TM.sequence_number=stt.sequence_number collate database_default
and TM.product_code=stt.product_code collate database_default
and stt.kind = 'B'
truncate table #tablename
END
This is my code that currently does not work.
In SQL Server, you can phrase this as:
ALTER PROCEDURE [dbo].[STT_Card_Entry_Temp_Write] (
#CARD_NO as VarChar(20),
#tablename as table
) AS
BEGIN
declare #sql nvarchar(max);
set #sql = '
Update live.scheme.sttakedm
set adjustment_quantit = (TM.expected_quantity - TM.counted) * -1,
take_sign = (case left((TM.expected_quantity - TM.counted), 1)
when ''-'' then ''+'' else ''-''
end),
status = ''''
from live.scheme.sttakedm stt join
#tablename TM
on TM.card_number = #CARD_NO and
TM.sequence_number = stt.sequence_number collate database_default and
TM.product_code = stt.product_code collate database_default and
stt.kind = 'B'
';
set #sql = replace(#sql, '#tablename', quotename(#tablename));
exec sp_executesql #sql,
N'#CARD_NO varchar(20)',
#CARD_NO=#CARD_NO;
set #sql = 'truncate table #tablename';
set #sql = replace(#sql, '#tablename', quotename(#tablename));
exec sp_executesql #sql;
END;
I would strongly advise you from using truncate table in dynamic SQL. You can really cause some problems.
If you are truncating the table, then why not just have a canonical table name used for passing values in? Or -- better yet -- create a table type so you can pass in the table directly.
You don't mention the database engine you're using, but the feature you need is called "dynamic SQL"; Google is your friend.
I'd be very nervous about allowing truncate table #tablename in dynamic SQL, though - it's really inviting disastrous bugs.
Related
I am running a cursor that executes dynamic SQL using a variable.
SET #Wk = CAST(#Cntr AS nvarchar(5))
DECLARE #params nvarchar(30) = N'#Wk nvarchar(5)'
-- .. start cursor
EXEC sp_executesql N'ALTER TABLE #Temp DROP COLUMN [WK #WK Sold]', #params, #Wk
I get the error
Msg 4924, Level 16, State 1, Line 4
ALTER TABLE DROP COLUMN failed because column 'WK #WK Sold' does not exist in table #Temp
I know that #param and #Wk work because I ran
EXEC sp_executesql N'select #Wk', #params, #Wk
and it worked. I know I can just run
EXEC ('ALTER TABLE #Temp DROP COLUMN [WK ' + #Wk + ' Sold]')
but I'd like to use sp_executesql.
Is it even possible the way I have tried?
Thank you
The problem has nothing to do with the variable here. The problem is that you think that a variable/parameter when used in the context of an object results in that variable being injected into the statement. That doesn't happen. SELECT 1 AS [#a] return return a column aliased as literally #a not a column where the alias is the value of a variable called #a.
What you need to do here is safely inject the value into the dynamic statement and ideally validate the name too:
DECLARE #Cntr SomeDataType; --I don't know what the datatype should be.
DECLARE #wk nvarchar(5),
#Column sysname;
SET #wk = #Cntr; --No need for the cast here
SET #Column = (SELECT c.name
FROM tempdb.sys.tables t
JOIN tempdb.sys.columns c ON t.object_id = c.object_id
WHERE t.[name] LIKE N'#temp%'
AND c.name = N'WK
' + #wk + N' Sold');--Does your column name really have a carriage return and line break in it?
DECLARE #SQL nvarchar(MAX) = N'ALTER TABLE #Temp DROP COLUMN ' + QUOTENAME(#Column) + N';';
EXEC sys.sp_executesql #SQL;
I'm trying to store DDLs of some views and stored procedures in a separate table in the Dump database. There are too many similar databases on the server. But some objects are redundant. They will be listed in the table and then dropped from the database. But their DDLs will be backed up if somebody will need them later.
The procedure works fine when the views are small, but if the size of the code exceeds some value - I'm get an error:
XML parsing: line 120, character 31, incorrect CDATA section syntax
Maybe that's I'm using the dbo.sp_sqlexec procedure, but I'm not sure. Will appreciate any ideas.
Definition of the table where those views will be firstly listed and then stored:
CREATE TABLE [dbo].[ViewList_Explicit]
(
[ID] [int] IDENTITY(1,1) PRIMARY KEY NOT NULL,
[ServerName] [sysname] NOT NULL,
[DatabaseName] [sysname] NOT NULL,
[SchemaName] [sysname] NOT NULL,
[ViewName] [sysname] NOT NULL,
[DefinitionText] [xml] NULL,
[IsTransferred] [bit] NOT NULL,
[DateTransferred] [datetime] NULL
);
INSERT INTO [dbo].[ViewList_Explicit] ([ServerName], [DatabaseName], [SchemaName], [ViewName], [DefinitionText], [IsTransferred], [DateTransferred])
VALUES ('dbserver', 'reco', 'dbo', 'v_redundant_toDrop', NULL, 0, NULL)
This is the code of the procedure:
CREATE OR ALTER PROCEDURE [dbo].[sp_moveViews2Dump]
(#DatabaseName SYSNAME)
AS
BEGIN
SET NOCOUNT ON
DECLARE #Serv SYSNAME = ##SERVERNAME;
DECLARE #SQLstringToDrop NVARCHAR(MAX), #SQLstringForDefinition NVARCHAR(MAX);
DECLARE #SchemaName SYSNAME, #ViewName SYSNAME, #ExplicitID INTEGER;
DECLARE #DDLview XML;
DECLARE #Buffer TABLE(line XML);
DECLARE Schedule_cursor CURSOR LOCAL FOR
SELECT ID, SchemaName, ViewName
FROM [Dump].dbo.ViewList_Explicit
WHERE DatabaseName = #DatabaseName
AND ServerName = #Serv
AND IsTransferred = 0
OPEN Schedule_cursor
FETCH NEXT FROM Schedule_cursor INTO #ExplicitID, #SchemaName, #ViewName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQLstringForDefinition = 'SELECT CONCAT(''<query><![CDATA['', VIEW_DEFINITION, '']]></query>'') FROM ['
+ #DatabaseName + '].INFORMATION_SCHEMA.VIEWS
WHERE TABLE_NAME = '''+ #ViewName + ''' AND TABLE_SCHEMA = ''' + #SchemaName + ''';'
--PRINT #SQLstringForDefinition
INSERT #Buffer EXECUTE dbo.sp_sqlexec #SQLstringForDefinition
SELECT #DDLview = line FROM #Buffer
SELECT #SQLstringToDrop = 'USE [' + #DatabaseName + ']
DROP VIEW IF EXISTS [' + #SchemaName + '].[' + #ViewName + ']'
--EXECUTE dbo.sp_sqlexec #SQLstringToDrop -- Commented out to avoid the deletion
UPDATE [Dump].dbo.ViewList_Explicit
SET [DefinitionText] = #DDLview, IsTransferred = 1, DateTransferred = GETDATE()
WHERE ID = #ExplicitID
DELETE FROM #Buffer
FETCH NEXT FROM Schedule_cursor INTO #ExplicitID, #SchemaName, #ViewName
END
CLOSE Schedule_cursor
DEALLOCATE Schedule_cursor
SET NOCOUNT OFF
END
Not sure why you're storing module definitions as XML but you should be able to do this in one step, without the cursor, unsupported system procedures from decades ago, and INFORMATION_SCHEMA which is generally garbage (<-- see the section on Module Definitions):
DECLARE #exec nvarchar(1024) = QUOTENAME(#DatabaseName)
+ N'.sys.sp_executesql';
DECLARE #sql nvarchar(max) = N';WITH vws AS
(SELECT SchemaName = s.name, ViewName = v.name,
DefinitionText = CONCAT(''<query><![CDATA['',
OBJECT_DEFINITION(v.[object_id]), N'']]></query>'')
FROM sys.schemas AS s
INNER JOIN sys.views AS v
ON s.[schema_id] = v.[schema_id]
)
UPDATE vle
SET vle.DefinitionText = sps.DefinitionText,
vle.IsTransferred = 1,
vle.DateTransferred = GETDATE()
FROM [Dump].dbo.ViewList_Explicit AS vle
INNER JOIN vws
ON vle.SchemaName = vws.SchemaName
AND vle.ViewName = vws.ViewName
WHERE vle.DatabaseName = #db
AND vle.ServerName = ##SERVERNAME
AND vle.IsTransferred = 0;';
EXEC #exec #sql, N'#db sysname', #DatabaseName;
The main problem was mentioned in a comment already: VIEW_DEFINITION is limited to 4,000 characters.
The purpose of #SQLstringToDrop is unclear. If you're on a modern enough version of SQL Server, you could instead inject CREATE OR ALTER into the definition, or generate that only at time of actual execution / deployment ... that doesn't change per view so there's no reason to store the entire IF EXISTS / DROP sequence for each and every view.
If you want to drop the views after you've backed up their definitions (though, really, why you aren't using proper version control system for this is a mystery), you can simply use the same technique (all in one shot instead of in a loop):
DECLARE #exec nvarchar(1024) = QUOTENAME(#DatabaseName)
+ N'.sys.sp_executesql';
DECLARE #sql nvarchar(max) = N'';
SELECT #sql += CONCAT(N'DROP VIEW IF EXISTS ',
QUOTENAME(SchemaName), N'.',
QUOTENAME(ViewName), N';')
FROM [Dump].dbo.ViewList_Explicit
WHERE DatabaseName = #DatabaseName
AND ServerName = ##SERVERNAME;
EXEC #exec #sql;
As an additional tip, don't ever put square brackets around names manually (e.g. [' + #SchemaName + ']) - this does not protect you against SQL injection. And while it's unlikely someone put nefarious object names into the system you're working against now, it sets a bad example.
I'm trying to fetch the data in a specific table name by passing tableName as a parameter to the stored procedure.
CREATE PROCEDURE schemaName.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SET #sql = 'SELECT * FROM ' + #tableName
EXECUTE sp_executesql #sql
END;
--> EXEC schemaName.spDynamicTableName 'Employee';
Now, how can I pass list of table names to a procedure so that procedure will iterate over the list of table names and fetch the data from all the tables?
Ok, let's start off with the problems you have in your current set up. Firstly it sounds like you have a design flaw here. Most likely you are using a table's name to infer information that should be in a column. For example perhaps you have different tables for each client. In such a scenario the client's name should be a column in a singular table. This makes querying your data significantly easier and allows for good use for key constraints as well.
Next, your procedure. This is a huge security hole. The value of your dynamic object is not sanitised nor validated meaning that someone (malicious) has almost 100 characters to mess with your instance and inject SQL into it. There are many articles out there that explain how to inject securely (including by myself), and I'm going to cover a couple of processes here.
Note that, as per my original paragraph, you likely really have a design flaw, and so that is the real solution here. We can't address that in the answers here though, as we have no details of the data you are dealing with.
Fixing the injection
Injecting Securely
The basic's of injecting a dynamic object name is to make it secure. You do that by using QUOTENAME; it both delimit identifies the object name and escapes any needed characters. For example QUOTENAME(N'MyTable') would return an nvarchar with the value [MyTable] and QUOTENAME(N'My Alias"; SELECT * FROM sys.tables','"') would return the nvarchar value "My Alias""; SELECT U FROM sys.tables".
Validating the value
You can easily validate a value by checking that the object actually exists. I prefer to do this with the sys objects, so something like this would work:
SELECT #SchemaName = s.[name],
#TableName = t.[name]
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
WHERE s.[name] = #Schema --This is a parameter
AND t.[name] = #Table; --This is a parameter
As a result, if the FROM returns no values, then the 2 variables in the SELECT won't have a value assigned and no SQL will be run (as {String} + NULL = NULL).
The Solution
Table Type Parameter
So, to allow for multiple tables, we need a table type parameter. I would create one with both the schema and table name in the columns, but we can default the schema name.
CREATE TYPE dbo.Objects AS table (SchemaName sysname DEFAULT N'dbo',
TableName sysname); --sysname is a sysnonym for nvarchar(128) NOT NULL
And you can DECLARE and INSERT into the TYPE as follows:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (TableName)
VALUES(N'test');
Creating the dynamic statement
Assuming you are using a supported version of SQL Server, you'll have access to STRING_AGG; this removes any kind of looping from the procedure, which is great for performance. If you're using a version only in extended support, then use the "old" FOR XML PATH method.
This means you can take the values and create a dynamic statement along the lines of the below:
SET #SQL = (SELECT STRING_AGG(N'SELECT * FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',' ')
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
The Stored Proecure
Putting all this together, this will give you a procedure that would look like this:
CREATE PROC schemaName.spDynamicTableName #Objects dbo.Objects AS
BEGIN
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = (SELECT STRING_AGG(N'SELECT N' + QUOTENAME(t.[name],'''') + N',* FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',#CRLF) --I also inject the table's name as a column
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
EXEC sys.sp_executesql #SQL;
END;
And then you would execute it along the lines of:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (SchemaName,TableName)
VALUES(N'dbo',N'MyTable'),
(N'dbo',N'AnotherTable');
EXEC schemaName.spDynamicTableName #Objects;
This one accepts a comma delimited list of tables and guards against SQL injection with a simple QUOTENAME escape (not sure if this is quite enough though):
IF OBJECT_ID('dbo.spDynamicTableName') IS NOT NULL DROP PROC dbo.spDynamicTableName
GO
/*
EXEC dbo.spDynamicTableName 'Students,Robert--
DROP TABLE Students'
*/
CREATE PROC dbo.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SELECT #sql = STRING_AGG('SELECT * FROM ' + QUOTENAME(value), ';')
FROM STRING_SPLIT(#tableName, ',')
--PRINT #sql
EXEC dbo.sp_executesql #sql
END;
GO
There are two ways you can do this: use a string that contains the names you want and are separated by a special character as:
Table1, Table2, Table3
and split it in the stored procedure (check this)
The second method: make a typo as follows:
CREATE TYPE [dbo].[StringList] AS TABLE
(
[TableName] [NVARCHAR(50)] NULL
)
Add a parameter for your stored procedure as StringList:
CREATE PROCEDURE schemaName.spDynamicTableName
#TableNames [dbo].[StringList] READONLY,
AS
BEGIN
END;
Then measure its length using the following code and make a repeat loop::
DECLARE #Counter INT
DECLARE #TableCount INT
SELECT #TableCount = Count(*), #Counter = 0 FROM #TableNames
WHILE #Counter < #TableCount
BEGIN
SELECT #TableName = Name
FROM #TableNames
ORDER BY Name
OFFSET #Counter ROWS FETCH NEXT 1 ROWS ONLY
SET #sql = 'SELECT * FROM ' + #TableName
EXECUTE sp_executesql #sql
SET #Counter = #Counter + 1
END
I know it's bad practice but the current situation requires a special character (sharp s = ß) as a column name. How can I write a SQL query which adds the column with the special character? With SSMS it adds Straße but when I run the SQL query with sqlcmd.exe through an external program it adds Straße.
This is the script:
DECLARE #Street varchar(50)='Straße';
IF NOT EXISTS(SELECT 1 FROM sys.columns
WHERE Name = N'#Street'
AND Object_ID = Object_ID(N'Document'))
BEGIN
EXECUTE
(
'ALTER TABLE dbo.Document ADD ' +
#Street + ' varchar(50) NULL
')
END
You need to use nvarchar not varchar:
DECLARE #Street sysname = N'Straße'; --synonym for nvarchar(128) NOT NULL
IF NOT EXISTS(SELECT 1 FROM sys.columns
WHERE Name = #Street --This shouldn't be a literal string
AND Object_ID = Object_ID(N'Document'))
BEGIN
DECLARE #SQL nvarchar(MAX);
SET #SQL - N'ALTER TABLE dbo.Document ADD ' QUOTENAME#(Street) + N' varchar(50) NULL;';
--Should the column be an nvarchar too, considering it's name?
EXEC sys.sp_executesql #SQL;
END;
This is my first post to StackOverflow. I've been using this amazing resource for a number of years to answer hundreds of SQL and PowerShell questions, however this one has me stumped for a number of days.
I am using SQL Server 2014 SP2 and I am trying to do an update to DATABASE1, FIELD1, then FIELD2 then FIELD3 from multiple other database.
FIELD1 may exist in one of multiple other databases.
FIELD1 may not exist in ALL databases - which is where I have the problem.
Database Design Link
I have the following (anonymised) query and it appears to be working:
EXEC sp_MSforeachdb 'IF ''?'' IN (''DATABASE2'',''DATABASE3'',''DATABASE4'')
BEGIN
UPDATE DATABASE1.PARAMETER
SET B.[VALUE] = A.[FIELD1]
FROM DATABASE1.TABLE1 B
INNER JOIN ?.dbo.[TABLE2] A
ON A.JOINVALUE = B.JOINVALUE
WHERE B.COLUMN2 = ''SOMETHING''
AND COLUMN3= ''PF.T.FIELD1''
END ;'
Until I get to say FIELD8, as it exists in DATABASE1 but not in DATABASE2, DATABASE3 or DATABASE4. I then get the following error:
Msg 207, Level 16, State 1, Line 30
Invalid column name 'FIELD8'.
From my Google and StackOverflow searches, I've tried to use (for the first time) a:
IF EXISTS (SELECT COLUMN1 FROM Database2.Table2 WHERE Column1='Field8')
EXEC .......
But that's where I started to really struggle.
Hope the above makes sense.
Any tips or assistance would be greatly appreciated.
N.B. I have about 3,000 fields in Database1 which require updating. I've so-far built all my UPDATE statements dynamically.
You can create stored proc, that will search for columns and tables in system tables:
ALTER PROCEDURE dbo.check_table_exists
-- Add the parameters for the stored procedure here
#table_name nvarchar(255),
#column_name nvarchar(255)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
DECLARE #SQLString nvarchar(max),
#ParmDefinition nvarchar(500) = N'#table_name nvarchar(255), #column_name nvarchar(255)';
IF OBJECT_ID(N'tempdb..#check_column_exists') is not null DROP TABLE #check_column_exists
CREATE TABLE #check_column_exists (
db nvarchar(500) NULL,
column_exists bit NULL
)
SELECT #SQLString =
(
SELECT N'USE '+QUOTENAME([name]) +'; '+
'INSERT INTO #check_column_exists '+
'SELECT '''+[name]+''' as [db], '+
' COUNT(*) as column_exists ' +
'FROM sys.tables t ' +
'INNER JOIN sys.columns c ' +
' ON t.[object_id] = c.[object_id] ' +
'WHERE t.[name] = #table_name and c.[name] = #column_name; '
FROM sys.databases
WHERE [name] NOT IN (
'msdb',
'model',
'tempdb',
'master'
)
FOR XML PATH('')
) + 'SELECT [db] FROM #check_column_exists WHERE column_exists = 1; DROP TABLE #check_column_exists;'
EXEC sp_executesql #SQLString, #ParmDefinition, #table_name = #table_name, #column_name = #column_name
END
GO
You can change it to search only for columns and output the database and table name or whatever.
The output is:
db
-----------
DATABASE1
DATABASE4
...
etc
After that you can write this to table and use for dynamic SQL update query:
DECLARE #table_name nvarchar(255) = 'SomeTable',
#column_name nvarchar(255) = 'SomeField'
DECLARE #results TABLE (
db nvarchar(500)
)
INSERT INTO #results
EXEC dbo.check_table_exists #table_name, #column_name
--...Here goes building of dynamic SQL query to update data
First, sp_MSforeachdb is not reliable. For a working alternative, check here: Making a more reliable and flexible sp_MSforeachdb - Aaron Bertrand
Second, you can use system views to check if a column exists in a given table using sys.columns like so:
if exists (
select 1
from sys.columns c
where c.name = 'pilots_id' /* column name */
and c.object_id = object_id(N'pilots') /* table name */
)
begin
select 'Pilots_Id exists' /* do stuff */
end
rextester demo: http://rextester.com/UUXCB18567