Script to create a schema using a variable - sql

I'm surprised that this hasn't come up before but I haven't found anything in searches either here or elsewhere. I found something vaguely similar which indicates that the problem is that the Use command in my script lasts only for that one line, but there was no indication of how to work around that.
What I'm trying to do: Create a generic script to create a "template" database with all of my common schemas and tables. All of the variables (such as the database name) are intended to be set in the header so that they can be changed as needed and the script can be just run without needing to do any risky search and replace operations to change hard coded values.
What the problem is: I can't get the schemas to generate in the right database; they're all generating in Master. Trying to explicitly set the database didn't help; I just received runtime errors.
My skill level: Long time Access user but still in the foothills of exploring SQL Server. I'm sure (well, hoping) this this will be ridiculously easy for someone further up the slope.
Does anyone know how I can do something like this? (Existing code shown below.)
DECLARE #DBName NVARCHAR(50) = 'TheDBName';
-- Assume that there's a bunch of code to drop and create the database goes here.
-- This code executes correctly.
SET #SQL = 'Use [' + #DBName + ']';
Print #SQL;
EXEC(#SQL);
SET #Counter = 1;
WHILE #Counter <=3
BEGIN
SET #SQL = 'CREATE SCHEMA [' +
CASE #Counter
WHEN 1 THEN 'Schema1'
WHEN 2 THEN 'Schema2'
WHEN 3 THEN 'Schema3'
END
SET #SQL = #SQL + '] AUTHORIZATION [dbo]';
PRINT 'Creating Schemas, ' + #SQL;
Exec(#SQL);
SET #Counter = #Counter + 1;
END

The use command only changes the current db in the scope you are in and dynamic SQL runs in a scope of its own.
Try this from master
declare #SQL nvarchar(max)
set #SQL = N'use tempdb; print db_name()'
exec(#SQL)
print db_name()
Result:
tempdb
master
Try this:
DECLARE #DBName NVARCHAR(50) = 'TheDBName';
DECLARE #SQL NVARCHAR(max)
DECLARE #SQLMain NVARCHAR(max)
DECLARE #Counter int
SET #SQLMain = 'Use [' + #DBName + ']; exec(#SQL)';
SET #Counter = 1;
WHILE #Counter <=3
BEGIN
SET #SQL = 'CREATE SCHEMA [' +
CASE #Counter
WHEN 1 THEN 'Schema1'
WHEN 2 THEN 'Schema2'
WHEN 3 THEN 'Schema3'
END
SET #SQL = #SQL + '] AUTHORIZATION [dbo]';
EXEC sp_executesql #SQLMain, N'#SQL nvarchar(max)', #SQL;
SET #Counter = #Counter + 1;
END

If you run USE statement inside EXEC() then run other statements also in EXEC()
but
you have to use USE databasename stmt. in every EXEC()

Other answers have explained your immediate problem but since this is really a deployment issue, as a general solution you might want to try using SQLCMD variables that you can then set at runtime from the command line. This would allow you to pass the database name and/or schema names into scripts dynamically so you can then automate your deployment using batch files or VS database projects.

Related

Setting SQL Variable via Dynamic SQL

I know I am overthinking this, but I've been banging against this for too long so I'm reaching out for help.
This is the statement I'm trying to run: SELECT #cntMax = MAX(id) FROM [Raw_Item-FieldReport]
BUT, the table name is a variable #reportTable
This doesn't work:
SET #sql = 'SELECT #cntMax = MAX(id) FROM #reportTable'
EXEC sp_executesql #sql
I even tried having the actual table name in the SET #sql and that doesn't work either.
I didn't think it would be this difficult, please tell me I'm missing something easy/obvious.
Here's the full bit of code for those who want it:
DECLARE
#inTable nvarchar(255) = 'Raw_Item',
#reportTable nvarchar(255),
#fieldName nvarchar(255),
#cnt int,
#cntMax int,
#sql nvarchar(max)
SET #reportTable = #inTable + '-FieldReport'
SET #cnt = 1
SELECT #cntMax = MAX(id) FROM [Raw_Item-FieldReport]
PRINT #cntMax
SET #cntMax = 0
SET #sql = 'SELECT #cntMax = MAX(id) FROM [Raw_Item-FieldReport]'
EXEC sp_executesql #sql
PRINT #cntMax
SQL Server 12.0.2008.8 (on Azure)
You need to use an output parameter, otherwise SQL Server has no idea how to connect #cntMax in the dynamic SQL to #cntMax not in the dynamic SQL, since they are different scopes. And to protect yourself from SQL injection (some tips here and here), always check that your object exists, and use QUOTENAME() as opposed to manually adding square brackets (and you should always use QUOTENAME() when building object names from user input or variables, even when they don't have bad characters like dashes):
DECLARE #sql nvarchar(max),
#inTable nvarchar(255) = N'Raw_Item',
#reportTable nvarchar(255);
SET #reportTable = N'dbo.' + QUOTENAME(#inTable + '-FieldReport');
IF OBJECT_ID(#reportTable) IS NOT NULL
BEGIN
SET #sql = N'SELECT #cntMax = MAX(id) FROM ' + #reportTable + N';';
EXEC sys.sp_executesql #sql,
N'#cntMax int output',
#cntMax = #cntMax OUTPUT;
PRINT #cntMax;
END
ELSE
BEGIN
PRINT 'Nice try, h#xx0rs!';
END
Always use schema reference (dbo), always use statement terminators, and please try to avoid naming things with invalid identifier characters like dash (-). And one additional tip: always use N prefix on N'nvarchar string literals'.

How to Create DELETE Statement Stored Procedure Using TableName, ColumnName, and ColumnValue as Passing Parameters

Here is what i'm trying to do. I'm trying to create a stored procedure where I could just enter the name of the table, column, and column value and it will delete any records associated with that value in that table. Is there a simple way to do this? I don't know too much about SQL and still learning about it.
Here is what I have so far.
ALTER PROCEDURE [dbo].[name of stored procedure]
#TABLE_NAME varchar(50),
#COLUMN_NAME varchar(50),
#VALUE varchar(5)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #RowsDeleted int;
DECLARE #sql VARCHAR(500);
SET #sql = 'DELETE FROM (name of table).' + #TABLE_NAME + ' WHERE ' + #COLUMN_NAME + '=' + '#VALUE'
EXEC(#sql)
SET #RowsDeleted=##ROWCOUNT
END
GO
Couple issues
First, you don't need (name of table)
SET #sql = 'DELETE FROM ' + #TABLE_NAME + etc.
In general you should try to include the appropriate schema prefix
SET #sql = 'DELETE FROM dbo.' + #TABLE_NAME + etc.
And in case your table name has special characters perhaps it should be enclosed in brackets
SET #sql = 'DELETE FROM dbo.[' + #TABLE_NAME + ']' + etc.
Since #Value is a string, you must surround it with single quotes when computing the value for #SQL. To insert a single quote into a string you have to escape it by using two single quotes, like this:
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + #VALUE + ''''
If #VALUE itself contains a single quote, this whole thing will break, so you need to escape that as well
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + REPLACE(#VALUE,'''','''''') + ''''
Also, ##ROWCOUNT will not populate from EXEC. If you want to be able to read ##ROWCOUNT, use sp_ExecuteSQL instead
EXEC sp_ExecuteSql #SQL
And finally, let me editorialize for a minute--
This sort of stored procedure is not a great idea. I know it seems pretty cool because it is flexible, and that kind of thinking is usually smart when it comes to other languages, but in the database world this approach causes problems, e.g. there are security issues (e.g. injection, and the fact that you need elevated privileges to call sp_executeSql) and there issues with precompilation/performance (because the SQL isn't known ahead of time, SQL Server will need to generate a new query plan each and every time you call this) and since the caller can supply any value for table and column name you have no idea whether this delete statement will be efficient and use indexes or if it will cause a huge performance issue because the table is large and the column is not indexed.
The proper approach is to have a series of appropriate stored procedures with strongly-typed inputs that are specific to each data use case where you need to delete based on criteria. Database engineers should not be trying to make things flexible; you should be forcing people to think through what exactly they are going to need, and implement that and only that. That is the only way to ensure people are following the rules, keeping R/I intact, efficient use of indexes, etc.
Yes, this may seem like repetitive and redundant work, but c'est la vie. There are tools available to generate the code for CRUD operations if you don't like the extra typing.
In addition to some of the information John Wu provided you have to worry about data types and ##ROWCOUNT may not be accurate if there are triggers on your tables and things..... You can get around both of those issues though by casting to nvarchar() and using OUTPUT clause with a temp table to do the COUNT().
So just for fun here is a way you can do it:
CREATE PROCEDURE dbo.[ProcName]
#TableName SYSNAME
,#ColumnName SYSNAME
,#Value NVARCHAR(MAX)
,#RecordCount INT OUTPUT
AS
BEGIN
DECLARE #SQL NVARCHAR(1000)
SET #SQL = N'IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END
CREATE TABLE #DeletedOutput (
ID INT IDENTITY(1,1)
ColumnValue NVARCHAR(MAX)
)
DELETE FROM dbo.' + QUOTENAME(#TableName) + '
OUTPUT deleted.' + QUOTENAME(#ColumnName) + ' INTO #DeletedOutput (ColumnValue)
WHERE CAST(' + QUOTENAME(#ColumnName) + ' AS NVARCHAR(MAX)) = ' + CHAR(39) + #Value + CHAR(39) + '
SELECT #RecordCountOUT = COUNT(ID) FROM #DeletedOutput
IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END'
DECLARE #ParmDefinition NVARCHAR(200) = N'#RecordCountOUT INT OUTPUT'
EXECUTE sp_executesql #SQL, #ParmDefinition, #RecordCountOUT = #RecordCount OUTPUT
END
So the use of QOUTENAME will help against the injection attack but not be perfect. And I use CHAR(39) instead of the escape sequence for a single quote on value because I find it easier when string building at that point.... By using Parameter OUTPUT from sp_executesql you can still return your count.
Keep in mind just because you can do something in SQL doesn't always mean you should.

Why I cannot change database dynamically SQL Server 2008

The following is not working and I am definitely missing the obvious but would be nice if somebody could explain why is not working. I need to change db dynamically.
The print out looks good but does not change db in the SQL Server drop down.
DECLARE #tempSql nvarchar(4000);
DECLARE #FinalSQL nvarchar(4000);
DECLARE #dbName varchar(100);
SET #dbName = 'Pubs';
SET #tempSql = 'SELECT DB_NAME()';
SET #FinalSQL = 'USE ' + #dbName + '; EXEC sp_executesql N''' + #tempSql + '''';
EXEC (#FinalSQL)
If SQLCMD mode is an option for your (within SSMS, for example), you can do this:
:setvar dbname Pubs
USE [$(dbname)]
SELECT DB_NAME()
Or, your original syntax was pretty close. Try this:
DECLARE #db AS NVARCHAR(258);
SET #db = QUOTENAME(N'Pubs');
EXEC(N'USE ' + #db + N'; EXEC(''SELECT DB_NAME();'');');
GO
There is a way to access data from a specific database by using this syntax :
FROM DatabaseName..TableName
maybe you should use a dynamic database name in your scripts, then change it whenever you need
otherwise, take a look at this : http://www.sqlteam.com/article/selecting-data-from-different-databases
Executing the dynamic SQL is done in a scope of its own.
So you do change the current database, as you see, but only within the scope of the dynamic SQL.

IF EXISTS in SQL Server Cursor Not Working

I have a cursor which works fine but when it gets to this part of the script, it seems to still run the update even though the table doesn't exists:
SET #sql = 'IF (EXISTS (SELECT * FROM ps_vars_' + #datasetid + '))
BEGIN
UPDATE ps_vars_' + #datasetid + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'')
END';
EXEC SP_EXECUTESQL #sql
What am I missing? The #datasetid variable gets passed in correctly too.
DECLARE #tablename sysname
SET #tablename = 'ps_vars' + #datasetid
IF (OBJECT_ID(#tablename, 'U') IS NOT NULL)
BEGIN
SET #sql = ' UPDATE ' + QUOTENAME(#tablename) + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'') ';
EXEC sp_executesql #sql
END
When you use the EXISTS with the table name to see if the table exists you're actually trying to access the table - which doesn't exist. That's why you're getting an error, not because of your UPDATE statement.
Try this instead:
SET #sql = 'IF (OBJECT_ID(''ps_vars_' + #datasetid + ''') IS NOT NULL)
BEGIN
UPDATE ...
END'
Then think about what might be wrong with your database design that requires you to use dynamic SQL like this. Maybe your design is exactly how it needs to be, but in my experience 9 out of 10 times (probably much more) this kind of code is a symptom of a poor design.

How to use a varying database?

I want to use a database which name is stored in a variable. How do I do this?
I first thought this would work but it doesn't:
exec('use '+#db)
That will not change database context
Suggestions anyone?
Unfortunately I don't know of a direct solution to this one. The nearest working version is:
DECLARE #db nvarchar(MAX)
SET #db = 'use DBname'
Exec sp_executesql #db
but this only changes the context for the length of the procedure call. However, more statements can be included in that call to make use of the context:
DECLARE #sql nvarchar(MAX)
SET #sql = 'use DBName SELECT * FROM Table1'
Exec sp_executesql #sql
If you absolutely have to do this using dynamic SQl, I prefer this:
DECLARE #sql nvarchar(MAX)
declare #databasename varchar (20)
Set #databasename = mydatabase
SET #sql = 'SELECT * FROM ' + #databasename + 'dbo.Table1'
Exec sp_executesql #sql
The reason I prefer it is that you can extend it to use multipe datbases in the same query if need be.
I havea a concern that you don't know the datbase name for each table already without resorting to dynamic means. In other words, why can't you write:
SELECT * FROM mydatabase.dbo.Table1
If you have multiple databases with the same table names, likely you have a design problem.
The use statement is only in scope inside the exec block. Therefore you would have to do everything else in the same exec:
exec('use '+ #db + '
--do other stuff'
)
Presumably you know all the possible database names. One (slightly inelligant) way of doing this would be to use a CASE or multiple IF statements to test the variable and hardcode the USE statement for each case.