T-SQL Cursor to update several columns in several tables - sql-server

I'm working with a customer who somehow loaded lowercase item numbers into a series of SQL tables. This should not be possible in this particular application and is causing all sorts of issues. I set out to update the items to the upper case versions one at a time but quickly realized that this would take forever. So I did the unthinkable and tried to use a cursor to help me out, however I keep stumbling over an error that has me baffled. Here's my code:
declare #tablename varchar(10)
declare upper_case cursor for
SELECT sys.objects.NAME
FROM sys.columns
INNER JOIN sys.objects ON sys.columns.object_id = sys.objects.object_id
WHERE sys.columns.NAME = 'item'
AND sys.objects.type = 'U'
OPEN upper_case
FETCH NEXT FROM upper_case into #tablename
UPDATE #tablename
SET item = upper(item)
CLOSE upper_case
DEALLOCATE upper_case
And here's the error:
Msg 1087, Level 16, State 1, Line 13
Must declare the table variable "#tablename".
I'm not using #tablename as a table variable, I'm trying to use it as a scalar variable but I thought, what the heck, I'll bite. So I switched it to a table variable:
declare #tablename table (tablename varchar(10))
And then I get this error:
Msg 137, Level 16, State 1, Line 5
Must declare the scalar variable "#tablename".
What am I missing? Am I not allowed to use a variable in an UPDATE statement? I know that each UPDATE can only update one table, but I thought that by using the cursor, I was effectively issuing multiple updates, each which only updates one table. Did I misunderstand that?
Here's the result that worked. I can't believe I'm the first to want to do this and I doubt I'll be the last:
DECLARE #tablename varchar(10)
DECLARE #sql nvarchar(max)
declare upper_case cursor for
SELECT sys.objects.NAME
FROM sys.columns
INNER JOIN sys.objects ON sys.columns.object_id = sys.objects.object_id
WHERE sys.columns.NAME = 'item'
AND sys.objects.type = 'U'
OPEN upper_case
FETCH upper_case into #tablename
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sql = 'UPDATE [' + #tablename + '] SET item = upper(item)'
EXEC(#sql)
FETCH upper_case into #tablename
END
CLOSE upper_case
DEALLOCATE upper_case

UPDATE you cannot pass table name as variable like you try:
UPDATE #tablename
SET item = upper(item);
Msg 1087, Level 16, State 1, Line 13 Must declare the table variable
"#tablename"
But UPDATE can work with table variables DECLARE #t TABLE = ... (not your case).
To use it how you want you need Dynamic-SQL like:
EXEC('UPDATE [' + #tablename + '] SET item = UPPER(item)');

As lad2025 points out, you need to use Dynamic SQL techniques to accomplish what you're trying to do. Two other points:
First, you're only updating the first result your cursor returns. You need to use a while loop to iterate through the cursor results, like so:
....
FETCH NEXT FROM upper_case into #tablename
WHILE ##FETCH_STATUS = 0
BEGIN
<do dynamic update>
FETCH NEXT FROM upper_case into #tablename
END
<close & deallocate>
Second, I would really encourage use of the INFORMATION_SCHEMA views rather than directly querying system tables. For your purposes they provide more than enough information, and are more readable and stable across SQL versions to boot.
The system metadata in INFORMATION_SCHEMA is a SQL-92 standard. It's stable across MS-SQL version and other ANSI-SQL-compliant engines. See also: INFORMATION_SCHEMA vs sysobjects. The main downside for writing quick scripts is that you have to type "information_schema" instead of just "sys". Eventually muscle-memory kicks in :)

Related

Calling dynamic SQL or stored procedure

I'm using a search object query (found on the internet, wish I could give credit to the developer) to search database for the columns needed when I write queries. The output search object query allows me to enter the type of table to look in (partial name) as well as the column name (partial name) I'm trying to find. I've been attempting to modify the search object query so it returns the 1st value (Top 1) it finds. This would help me to easily see at a glance if the column has the particular type of data I'm looking for.
I've attempted to write it both as a stored procedure that I could pass two parameters (partial table and partial column name) and I've also tried using dynamic SQL (my first attempt at using it, so I'm a novice when it comes to use it). I had moderate success with the use of dynamic SQL, but can only get it to produce one result rather than be called multiple times for all the results in my search object output. The code I used is shown here:
-- This is the search object query found on internet
Use masterdb
Select a.name, b.name
From sysobjects a
Inner Join syscolumns b On a.id = b.id
Where b.name like '%Result%'
And a.name like '%Lab%'
Order By a.name, b.name
-- This is a separate query I used to test calling the data with dynamic SQL
DECLARE #value VARCHAR(100), #tablename VARCHAR(100)
SET #value = 'Result'
SET #tablename = 'LabSpecimen'
DECLARE #sqlText NVARCHAR(1000);
SET #sqlText = N'SELECT Top 1 ' + #value + ' FROM testndb.dbo.' + #tablename
EXEC (#sqlText)
If I use the search object query and search for tables that have lab and column names that have result, I might get output like this:
LabMain,ResultID
LabSpecimen,ResultCategory
LabSpecimen,ResultDate
LabSpecimen,Results
I would like to have the search object query pull data from the table in the first column and the column name in the 2nd column and return the first value it finds to give me a sample output for the given column name/table. Output would look like this:
LabMain,ResultID,E201812310001
LabSpecimen,ResultCategory,ExampleCategory
LabSpecimen,ResultDate,20181231
LabSpecimen,Results,34.20
Okay, I really didn't want to have to post an answer to this, but here goes.
So, the first, really-really-huge thing is: SQL Injection. SQL Injection is the #1 security vulnerability for something like a dozen years running, per OWASP. Basically, SQL Injection is where you use dynamic SQL that has any fragment of the sql command being populated by a user. So in the OP's case, this section here:
SET #value = 'Result'
SET #tablename = 'LabSpecimen'
DECLARE #sqlText NVARCHAR(1000);
SET #sqlText = N'SELECT Top 1 ' + #value + ' FROM testndb.dbo.' + #tablename
EXEC (#sqlText)
... if the end incarnation would be that #tableName and #value are populated by the user as part of their search? Then the user can do a 'search' that ends up injecting sql statements that the server runs directly; for a cheap example, imagine this for #value:
3' ; drop table #tableName --
... which would go ahead and drop every table that matches the #tablename you passed in.
Anyway, so, as we go through this problem, we're going to keep SQL Injection in mind at every step.
Problem #1: How to get the tables/columns that match.
You pretty much already nailed this. The only thing missing is to put it into a temp table so that you can loop through it (and limit it down to U-types, since otherwise you'll get stored procs and system tables.) I went ahead and had it also hit the Schema information - that way, if you have tables in different schemas, it'll still be able to get the results.
declare #tableNameFragment varchar(100) -- note: these top 4 lines will eventually
declare #columnNameFragment varchar(100) -- be changed to stored proc args
set #tableNameFragment = 'Performance' -- and populated by the user calling
set #columnNameFragment = 'status' -- the proc (instead of hard-coded.)
declare #entityMatches TABLE (TableName varchar(200), ColName varchar(128))
insert into #entityMatches
Select sch.TABLE_SCHEMA + '.' + sysobj.name as TableName, syscol.name as ColName
From sysobjects sysobj
Join syscolumns syscol On sysobj.id = syscol.id
Join INFORMATION_SCHEMA.TABLES sch on sch.TABLE_NAME = sysobj.name
where sysobj.xtype = 'U'
and (sysobj.name like '%' + isnull(#tableNameFragment,'') + '%')
and (syscol.name like '%' + isnull(#columnNameFragment,'') + '%')
Now, notice that while #tableNameFragment and #columnNameFragment are used, they're not used in a dynamic query. It doesn't matter if the user puts in something malicious into those values
Problem #2 - How to loop through your table
Basically, you're going to need a cursor. I hate cursors, but sometimes (like this one), they're necessary.
Problem #3 - How to actually do a dynamic query and get a result back
This is actually trickier than it looks. You can't do a raw EXEC() for a return value, nor can you simply have the cmd you're executing populating a variable - because EXEC (and SP_ExecuteSql operate in a different context, so they can't populate variables outside in your script.)
You need to use SP_ExecuteSQL, but specify a return variable getting populated by the interior sql command. For example:
declare #sqlCmd nvarchar(max)
declare #dynamicReturn varchar(max)
set #sqlCmd = 'select #retVal=1'
EXEC Sp_executesql #sqlCmd,
N'#retVal varchar(max) output',
#dynamicReturn output
select #dynamicReturn
Problem #4 - How to write your Dynamic command
Here's where things get dicey, since it's where we're using a dynamic SQL command. The important thing here is: you cannot use anything the user provided as an input. Which means, you can't use the variables #tableNameFragment or #columnNameFragment. You can use the values in the #entityMatches table, though. Why? Because the user didn't populate them. They got populated by the data in the sys tables - it doesn't matter if the user puts something nefarious in the input variables, that #entityMatches data simply holds the existing table/column names that match.
Also important: When you're working on code that could be a problem if a future dev down the line tweaks or copies/pastes - you should put comment warnings to illuminate the issue.
So, putting it all together? You'll have something that looks like this:
declare #tableNameFragment varchar(100) -- note: these top 4 lines will eventually
declare #columnNameFragment varchar(100) -- be changed to stored proc args
set #tableNameFragment = 'Performance' -- and populated by the user calling
set #columnNameFragment = 'status' -- the proc (instead of hard-coded.)
declare #entityMatches TABLE (TableName varchar(200), ColName varchar(128))
insert into #entityMatches
Select sch.TABLE_SCHEMA + '.' + sysobj.name as TableName, syscol.name as ColName
From sysobjects sysobj
Join syscolumns syscol On sysobj.id = syscol.id
Join INFORMATION_SCHEMA.TABLES sch on sch.TABLE_NAME = sysobj.name
where sysobj.xtype = 'U'
and (sysobj.name like '%' + isnull(#tableNameFragment,'') + '%')
and (syscol.name like '%' + isnull(#columnNameFragment,'') + '%')
declare #returnResults TABLE (TableName varchar(200), ColName varchar(128), FirstValue varchar(max))
declare Cur Cursor For select TableName,ColName from #entityMatches
declare #cursorTable varchar(200), #cursorColumn varchar(128)
open Cur
fetch Next from cur into #cursorTable,#cursorColumn
while ##FETCH_STATUS = 0
begin
-- Note: the variables #cursorTable, #cursorColumn are NOT user populated
-- but instead are populated from the Sys tables. Because of this,
-- this dynamic sql below is not SQL-Injection vulnerable (the entries
-- are not populated from user entry of any sort.)
-- Be very careful modifying the lines below to make sure you don't
-- introduce a vulnerability.
declare #sqlCmd nvarchar(max)
declare #dynamicReturn varchar(max)
set #sqlCmd = 'select top 1 #retVal=[' + #cursorColumn + '] from ' + #cursorTable
EXEC Sp_executesql #sqlCmd,
N'#retVal varchar(max) output',
#dynamicReturn output
insert into #returnResults values (#cursorTable, #cursorColumn, #dynamicReturn)
fetch Next from cur into #cursorTable,#cursorColumn
End
close cur
deallocate cur
select * from #returnResults
Create a stored procedure like below mention stored procedure.
Get the table and column name from sysobject & syscolumn and add it in hash table on the base of parameter of stored procedure. After that declare a cursor and in loop of cursor create a dynamic query of column and table name and get first row of current column from table of cursor loop. After that execute the query and update the result in the hash table. At the end of lookup select the Record from hash table. Check the below stored procedure. I hope that its helpful for you.
Create procedure Sp_GetSampleData
#TName varchar(200) = ''
as
Select
a.name TableName, b.name ColumnName,
CAST('' as varchar(max)) as SampleValue
into
#Tbl
from
sysobjects a
inner join
syscolumns b on a.id = b.id
where
(#TName='' or a.name = #TName)
order ny
a.name, b.name
declare #TableName varchar(200), #ColumnName varchar(200),
#sqlText nvarchar(max), #Val varchar(max)
declare Cur Cursor For
select TableName, ColumnName
from #Tbl
open Cur
fetch Next from cur into #TableName,#ColumnName
while ##FETCH_STATUS =0
begin
set #sqlText=''
set #Val=''
SET #sqlText = N'SELECT Top 1 #Val=[' + #ColumnName + '] FROM testndb.dbo.' + #TableName
EXEC Sp_executesql
#sqlText,
N'#Val varchar(max) output',
#Val output
print #sqlText
update #Tbl set SampleValue=#Val where TableName=#TableName and ColumnName =#ColumnName
fetch Next from cur into #TableName,#ColumnName
End
close cur
deallocate cur
select * from #Tbl

SQL Server Cursor Error

I want find the records which have ~ in the table.
I am using cursor to, but I am getting unexpected error.
Any help would be appreciated.
DECLARE #test VARCHAR(5000)
DECLARE #column_name VARCHAR(2000)
Declare #TABLE_NAME_MAIN VARCHAR(200)
SET #TABLE_NAME_MAIN = 'Ar_Receipt_Item_OHM'
DECLARE cur_name
CURSOR FOR
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TABLE_NAME_MAIN
OPEN cur_name
FETCH NEXT FROM cur_name INTO #column_name
WHILE ##Fetch_status = 0
BEGIN
SET #test = N'SELECT top 2 * FROM OHMPreStage.dbo.'+#TABLE_NAME_MAIN+' WHERE '+#column_name+' LIKE ''%~%'''
exec #test
FETCH NEXT FROM cur_name INTO #column_name
END
CLOSE cur_name
DEALLOCATE cur_name
SET NOCOUNT OFF
Error:
Msg 911, Level 16, State 4, Line 24
Database 'SELECT top 2 * FROM OHMPreStage' does not exist. Make sure that the name is entered correctly.
You just need to change:
exec #test
To
exec (#test)
EXEC is for executing stored procedures, EXEC() the function puts together a dynamic string and executes it.
You're probably going to have issues searching columns with "LIKE" That aren't string columns.
You should check out the answer to this question here, which gives you a script that scans all tables in a database for specific string content:
Selecting column names that have specified value
Just substitute your search string '%~%', and if you need to, limit the tables searched to just the one (or set of) tables you need.

How to create a stored procedure sql server to see all tables in a databse?

So far in the website I have only found stored procedures to list all the tables in a database, but what I really want to know is how to create a stored procedure in SQL Server to display all data of all tables in a specific database.
I don't want to join the tables and then display a huge table, I want to create some kind of loop that takes the first table of the database, performs a
SELECT * FROM <Table>
query, and then continues with the next table of the database and so on until all tables are displayed.
I know it should be easy but I have never created a stored procedure before so I don't know how to use the variables or go through the tables.
Thank you
Something like this should work:
CREATE Procedure [dbo].[procSelectAllFromAllTables]
AS
DECLARE #table nvarchar(500)
DECLARE #sql nvarchar(520)
DECLARE CursorSelect CURSOR FOR
select table_name from INFORMATION_SCHEMA.tables where table_name not like 'sys%'
OPEN CursorSelect
FETCH NEXT FROM CursorSelect
INTO #table
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sql = 'select * from ' + #table
exec(#sql)
FETCH NEXT FROM CursorSelect
INTO #table
END
CLOSE CursorSelect
DEALLOCATE CursorSelect
RETURN
learn how to create a stored procedure
learn how to use variables in a stored procedure
Get a list of all the table names
use a cursor to create a while loop on a list of all table names
use dynamic sql on 'select * from ' + #tablename
As others have said, this is a silly idea from a practical standpoint, but as an academic exercise, it is fairly simple if you use a bit of dynamic sql and COALESCE(). No cursors or loops required.
DECLARE #SQL VARCHAR(MAX)
SELECT #SQL = COALESCE(#SQL, '') + ' SELECT * FROM ' + s.name + '.' + t.name
FROM sys.tables t INNER JOIN sys.schemas s ON s.schema_id = t.schema_id
EXEC #SQL

MS SQL update same column in multiple tables using a cursor

I have a database containing a lot of tables, most of which have a column called contract which is a varchar(255) and is used as a foreign key. The following query selects all table names that contain this column (33 in total):
SELECT name FROM sys.objects WHERE object_id IN (
SELECT object_id FROM sys.columns WHERE Name = N'contract'
) AND name <> N'sysdercv'
Now, I need to change all of the contract names (not many) into anonymous identifiers so I can use screenshots without revealing sensitive data, but I need to maintain the relationships so the application still works. Essentially, I want to change all instances of contract='SomeCompany' to contract='example1' in all of the tables returned by the above query.
My initial attempt to do so is as follows:
declare #tablename sysname
declare C cursor fast_forward for
SELECT name FROM sys.objects WHERE object_id IN (
SELECT object_id FROM sys.columns WHERE Name = N'contract'
) AND name <> N'sysdercv'
open C
fetch next from C into #tablename
while ##fetch_status = 0
begin
update #tablename set contract='example1' where contract='SomeCompany'
fetch next from C into #tablename
end
close C
deallocate C
go
However, this gives the error Must declare the table variable "#tablename".
So my questions are:
Is what I am trying to do possible, or do I need to manually go through and update all the tables myself?
If so, am I on the right lines or am I barking up entirely the wrong proverbial tree?
In this case, the code thinks you have a table variable called #tablename
DECLARE #tablename TABLE (SomeCol...)
This won't work if you do this because object names in almost all DML or DDL can not be parameterised with variables.
You have to do this
SET #sql = 'update ' + #tablename +
' set contract=''example1'' where contract=''SomeCompany'''
EXEC (#sql)

Insert statement with cursor

In a table there are like 113 columns. and there are two default records in the table, one is for unknown and another is for inapplicable. So, each column has its own default value to represent unknown and inapplicable.
I dont wanna write regular insert statement to get those two records.
so, I tried to insert each column using a cursor.
Got the names of columns for that table from information_schema.columns and tried to insert values from exact table in another location using "insert into select" statement, but the name of the columns that we get from information_schema
Declare #col_name varchar(50)
declare my_cur CURSOR for
select column_name from information_schema.columns
where table_name = 'tabl' and table_catalog = 'db'
and table_schema = 'dbo'
Fetch next from my_cur
into #col_name
while ##FETCH_STATUS = 0
BEGIN
Insert into db.dbo.tabl (***#col_name***)
select ***#col_name*** from openrowset('sqlncli', 'server=my_server; trusted_connection=yes;', db.dbo.tabl)
fetch next from my_cur into #col_name
end
close my_cur
deallocate my_cur
go
But, I did not realize that #col_name would be treated as string, rather than object (column)
Is there any work around for this case or any alternative solution.
I think that getting these defaults populated is the least of your problems.
I'd suggest taking a look at this: Fundamentals of Relational Database Design
And if you still want to do this, it might be better to retrieve all the defaults from the linked server, place them in a temp table, and then join to information_schema.columns to populate your table. You'll probably need to transpose the data to make it work.
You will have to generate the INSERT statement as dynamic SQL and then execute it
Declare #InsertStatement VarChar (Max)
SET #InsertStatement = ''
SET #InsertStatement = #InsertStatement + ' Insert into db.dbo.tabl (' + #col_name + ') '
SET #InsertStatement = #InsertStatement + ' select ' + #col_name + ' from openrowset(''sqlncli'', ''server=my_server''; '
Exec Sp_SQLExec #InsertStatement

Resources