Create A View With Dynamic Sql - sql-server

I'm trying to create a dynamic database creation script.
There are a lot of steps and we create this database often so the script looks something like this.
DECLARE #databaseName nvarchar(100) = 'DatabaseName'
EXEC('/*A lot of database creation code built off of #databaseName*/')
This is all well and good except for one view that we'd like to create in #databaseName.
I've tried four different ways to create this view without success:
My first thought was to simply set the database context and then create the view in one script. Unfortunately, this didn't work because CREATE VIEW must be the first statement in its query block (details).
--Result: Error message, "'CREATE VIEW' must be the first statement in a query batch"
EXEC
('
USE [' + #databaseName + ']
CREATE VIEW
')
To get around (1) I tried to set the context separately so that CREATE VIEW would be the first command in the EXEC. This did create the view but did so within my current context and not #databaseName. It seem that the effects of calling USE in EXEC only persist until the end of that EXEC statement (details).
--Result: The view is created in the currently active database rather than #databaseName
EXEC ('USE [' + #databaseName + ']')
EXEC ('CREATE VIEW')
Next I tried putting everything back into one script but included a GO command in order to make CREATE VIEW the first command in a new query block. This failed because GO isn't allowed within an EXEC script (details).
--Result: Error message, "Incorrect syntax near 'GO'"
EXEC
('
USE [' + #databaseName + ']
GO
CREATE VIEW
')
Finally I tried to specify the target database as part of the CREATE VIEW command. In this case the script failed because CREATE VIEW doesn't allow the database to be specified as part of its creation (details).
--Result: Error message, "'CREATE/ALTER VIEW' does not allow specifying the database name as a prefix to the object name"
EXEC ('CREATE VIEW [' + #databaseName + '].[dbo].[ViewName]')
Any suggestions? I think this should be a common use case but Google wasn't able to help me.

You can do this by double nesting the dynamic SQL statements then:
begin tran
declare #sql nvarchar(max) =
N'use [AdventureWorks2012];
exec (''create view Test as select * from sys.databases'')';
exec (#sql);
select * from AdventureWorks2012.sys.views
where name = 'Test'
rollback tran

Instead of double nesting, another approach is to create a stored procedure whose only purpose is to executes dynamic SQL
CREATE PROCEDURE [dbo].[util_CreateViewWithDynamicSQL]
#sql nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
EXECUTE (#sql)
END
The stored procedure above can be re-used. Anytime you need to create a view just call the stored procedure and pass it the dynamic sql.
EXECUTE util_CreateViewWithDynamicSQL 'create view Test as select * from sys.databases'
I prefer this approach because dynamic sql is confusing enough and adding double nesting complicates it further.

Related

Copy a view definition from one database to another one in SQL Server

I am developping a script that allows me to create all the views present in database1 in another database (database2).
I am trying to do that using a cursor that loop on all the views of database1 and then try to execute the definition of that view on the second database.
Unfortunately it doesn't work. I get the following error:
Syntaxe incorrecte vers 'go'.
Msg 111, Niveau 15, État 1, Ligne 14
'CREATE VIEW' doit être la première instruction d'un traitement de requêtes.
This is my code
declare #database2 varchar(50), #database1 varchar(50)
set #database2 = 'Local'
set #database1 = 'prod'
declare #Query nvarchar(max), #view_definition nvarchar(max), #count int
set #count = 0
declare curseur cursor for SELECT top 1 view_definition FROM prod.information_schema.VIEWS
open curseur
fetch curseur into #view_definition
While ##FETCH_STATUS = 0
Begin
set #count = #count + 1
--Print 'Vue N° ' + cast(#count as varchar) + ':'
set #Query = N'Use ' + #database2 +CHAR(13)+CHAR(10)+ 'go' + #view_definition +CHAR(13)+CHAR(10)+ 'go'
print #Query
exec sp_executesql #Query
fetch curseur into #view_definition
End
Close curseur
deallocate curseur
This code was executed from database1.
However when I execute the result of the 'print #Query' instruction, it works!!
Does anyone can help me to resolve the problem?
Thanks in advance
There are two things here:
You can't USE another database in a stored procedure, although is the other database is on the same server you can use CREATE VIEW databasename.schemaname.viewname (.... If it's on another server, yiu could try setting it up as a linked server and using servername.database.schema.viewname.
sp_executesql expects one statement, and doesn't accept GO to my
knowledge (which is what the error is telling you). You could try to
put in ';' instead (and you don't need the CHAR(13)'s if you do
although they make it easier to read).
In case this helps someone in the future, here's my solution to this problem. The full history of how I came up with it is over at Stored Procedure to Copy Views from Current Database to Another
CREATE PROCEDURE [dbo].[usp_Copy_View_To_Database]
#ViewName SYSNAME, -- The name of the view to copy over
#DatabaseName SYSNAME, -- The name of the database to copy the view to
#overwrite bit = 1 -- Whether to overwrite any existing view
AS
IF DB_ID(#DatabaseName) IS NULL -- Validate the database name exists
BEGIN
RAISERROR('Invalid Destination Database Name passed',16,1)
RETURN
END
SET NOCOUNT ON
IF #overwrite = 1 -- If set to overwrite, try to drop the remote view
BEGIN
DECLARE #DropViewStatement NVARCHAR(MAX) =
'EXEC ' + QUOTENAME(#DatabaseName) + '.sys.sp_executesql N''DROP VIEW IF EXISTS ' + QUOTENAME(#ViewName) + ';'';'
EXEC (#DropViewStatement);
END
-- Extract the saved definition of the view
DECLARE #ViewDefinition NVARCHAR(MAX);
SELECT #ViewDefinition = definition FROM sys.sql_modules WHERE [object_id] = OBJECT_ID(#ViewName);
-- Check for a mismatch between the internal view name and the expected name (TODO: Resolve this automatically?)
IF #ViewDefinition NOT LIKE ('%' + #ViewName + '%')
BEGIN
DECLARE #InternalName NVARCHAR(MAX) = SUBSTRING(#ViewDefinition, 3, CHARINDEX(char(10), #ViewDefinition, 3)-4);
PRINT ('Warning: The view named '+#ViewName+' has an internal definition name that is different ('+#InternalName+'). This may have been caused by renaming the view after it was created. You will have to drop and recreate it with the correct name.')
END
-- Substitute any hard-coded references to the current database with the destination database
SET #ViewDefinition = REPLACE(#ViewDefinition, db_name(), #DatabaseName);
-- Generate the dynamic SQL that will create the view on the remote database
DECLARE #CreateViewStatement NVARCHAR(MAX) =
'EXEC ' + QUOTENAME(#DatabaseName) + '.sys.sp_executesql N''' + REPLACE(#ViewDefinition,'''','''''') + ''';'
--PRINT '#CreateViewStatement: ' + #CreateViewStatement -- Can be used for debugging
-- Execute the create statement
EXEC (#CreateViewStatement);
In short, you need two layers of nested dynamic SQL:
Inner layer to execute the "CREATE VIEW" statement, which must be on its own. This is executed using EXEC SomeDatabase.sys.sp_executesql #CreateViewSQL
One more layer to dynamically specify "SomeDatabase" via a parameter (assuming you require the flexibility of copying it to a database not known at scripting time).
Calling the above stored proc in the inner-loop of the original poster's tentative solution should solve the problem of copying a view to another database.
Note that simply looping over all views might pose a challenge if views depend on one another. There may be some additional complexity involving resolving the dependency tree of views an copying them in the correct order. Alternatively, a "dumb and easy" approach might be to loop over all views, ignore failures, and keep repeating the process until all views have been created.

Switching from one database to another within the same script

I would like to know how I can switch from one database to another within the same script. I have a script that reads the header information from a SQL Server .BAK file and loads the information into a test database. Once the information is in the temp table (Test database) I run the following script to get the database name.
This part works fine.
INSERT INTO #HeaderInfo EXEC('RESTORE HEADERONLY
FROM DISK = N''I:\TEST\database.bak''
WITH NOUNLOAD')
DECLARE #databasename varchar(128);
SET #databasename = (SELECT DatabaseName FROM #HeaderInfo);
The problem is when I try to run the following script nothing happens. The new database is never selected and the script is still on the test database.
EXEC ('USE '+ #databasename)
The goal is switch to the new database (USE NewDatabase) so that the other part of my script (DBCC CHECKDB) can run. This script checks the integrity of the database and saves the results to a temp table.
What am I doing wrong?
You can't expect a use statement to work in this fashion using dynamic SQL. Dynamic SQL is run in its own context, so as soon as it has executed, you're back to your original context. This means that you'd have to include your SQL statements in the same dynamic SQL execution, such as:
declare #db sysname = 'tempdb';
exec ('use ' + #db + '; dbcc checkdb;')
You can alternatively use fully qualified names for your DB objects and specify the database name in your dbcc command, even with a variable, as in:
declare #db sysname = 'tempdb';
dbcc checkdb (#db);
You can't do this because Exec scope is limited to dynamic query. When exec ends context is returned to original state. But context changes in Exec itself. So you should do your thing in one big dynamic statement like:
DECLARE #str NVARCHAR(MAX)
SET #str = 'select * from table1
USE DatabaseName
select * from table2'
EXEC (#str)

SQL Server / Create view from stored procedure

I am trying to create a view out of a stored procedure and am perplexed to see two opposing results from a very similar approach.
Example 1
CREATE PROCEDURE cv AS
GO
DECLARE #sql nvarchar(MAX)
SET #sql = 'CREATE VIEW test AS SELECT * FROM someOtherTable'
exec (#sql)
Whereas this example creates the view once the procedure is created for the 1st time, it will not recreate the view when I execute the procedure at a later stage using:
EXEC cv
Example 2
CREATE PROCEDURE cv
#table SYSNAME
AS
DECLARE #sql nvarchar(MAX)
SET #sql = 'CREATE VIEW '+ #table +' AS SELECT * FROM someOtherTable'
This one instead does not create the view when the procedure is created for the first time but creates the view afterwards every time it is called by:
EXEC #sql;
Why is this the case? I think this is really confusing and does not make sense or does it?
For your 1st statement
CREATE PROCEDURE cv AS
GO --<-- This GO here terminates the batch
DECLARE #sql nvarchar(MAX)
SET #sql = 'CREATE VIEW test AS SELECT * FROM someOtherTable'
exec (#sql)
the GO batch terminator create the procedure and the EXECUTES the following statement straightaway. So it appears to you as you have created a procedure which created the view for you.
Infact these are two statements in two batches.
--BATCH 1
CREATE PROCEDURE cv AS
GO
--BATCH 2
DECLARE #sql nvarchar(MAX)
SET #sql = 'CREATE VIEW test AS SELECT * FROM someOtherTable'
exec (#sql)
Batch 1 Creates a procedure which has nothing inside it, but it creates a procedure object for you with no functionality/Definition at all.
Statement after the key word GO is executed separately and creates the view for you.
My Suggestion
Always check for an object's existence before you create it. I would write the procedure something like this..
CREATE PROCEDURE cv
AS
BEGIN
SET NOCOUNT ON;
IF OBJECT_ID ('test', 'V') IS NOT NULL
BEGIN
DROP VIEW test
END
DECLARE #sql nvarchar(MAX)
SET #sql = 'CREATE VIEW test AS SELECT * FROM someOtherTable'
exec (#sql)
END
In Example 1 - you are creating a view with a hard-coded name of test. On subsequent runs of your proc, as the view already exists, SQL Server will throw an error because you are trying to create a view with the same name as one that already exists.
In Example 2, you are passing in the name of the view as a parameter. This will always create a new view unless the #table value you pass in corresponds to an existing view.
Just wondering - why are you creating a view with a stored proc? This is not something you would normally do in SQL.
You need to change
EXEC #sql to EXEC (sql)
EXEC can be use to run stored procedure, not dynamic sql
eg.
declare #dd varchar(100)='Select ''A'''
exec (#dd) ->will work fine
exec #dd -> Error
Read more about The Curse and Blessings of Dynamic SQL
Create view statement cannot be used with a stored procedure
EXEC ('CREATE VIEW ViewName AS SELECT * FROM OPENQUERY(Yourservername,''EXECUTE [databasename].[dbo].[sp_name] ''''' +Parameter + ''''''')')

Sharing stored procedure across database using synonyms

I have two different SQL Server databases (on the same server - if it helps) that need to share the same stored procedure logic. The solution I'm trying to achieve looks like this:
Database1
Table: TestTable
Synonym: sp_MyProc pointing at SharedDatabase.dbo.sp_MyProc
Database2
Table: TestTable
Synonym: sp_MyProc pointing at SharedDatabase.dbo.sp_MyProc
SharedDatabase
Proc: sp_MyProc which runs queries against TestTable
My hope was to use the synonyms so that if I execute sp_MyProc while in the context of Database1, it would use Database2.TestTable. And if I execute sp_MyProc while in the context of Database2, it would go against Database2.TestTable. However, when I execute sp_MyProc through either of the synonyms, it ignores the context of the synonym and executes looking for a local copy of TestTable, which is not found.
Is there a way to implement a shared stored procedure that executes against different copies of tables in different databases, either through synonyms or some other mechanism?
Edit
I should mention that in my case I am looking to do this with a large set of existing tables and procs, so any solution that requires modifying the procs or tables themselves are not ideal.
Something like this would work for the definition of the procedure. Be sure to guard against SQL injection since this is built dynamically.
CREATE PROCEDURE [dbo].dosomething
#databaseName sysname,
#schema sysname,
#tableName sysname
as
declare #cmd as nvarchar(max)
set #cmd = N'select * from ' + quotename(#schema) + N'.' + quotename(#tableName)
exec sp_executesql #cmd
Then use it like this:
dosomething 'SampleDb', 'dbo', 'sampleTable'
If the stored proc is in the SharedDatabase, then it will always run in context of SharedDatabase. To accomplish what you are trying to do to centralize code, I would maybe pass in a parameter to designate which server it is coming from, so then you can execute the query against that specific TestTable. Basically, you will need to refer to each table using their fully qualified name - i.e. Database1.dbo.TestTable
USE SharedDatabase
CREATE PROCEDURE [dbo].sp_MyProc
#dbsource varchar(50)
as
if(#dbsource == 'DB1')
begin
select * from Database1.dbo.TestTable
end
else
begin
select * from Database2.dbo.TestTable
end
GO
The other alternative is to make a view in SharedDatabase, which will be called TestTableComposite, with an extra column to identify where the source data is. And then pass that in as the parameter, and your SP on SharedDatabase will always be in context of that DB.

Is it possible to query the ##DBTS on a database other than the current database?

For a rigorous marker of the source database state, I'd like to capture the ##DBTS
of an external database in a sproc. Yeah, I think I could issue
USE ExternalDB
GO
SELECT #myVarbinary8 = ##DBTS
GO
USE OriginalDB
GO
but, even if I could, it seems ugly.
For now, I've embedded a scalar-valued function in the source database to invoke the
SET #Result = SELECT ##DBTS
which worked fine until I forgot to ask the DBA to grant the appropriate rights for a new user, which crashed a process.
Something akin to
SELECT ExternalServer.dbo.##DBTS
(I know that doesn't work).
See MSDN ##DBTS documentation
##DBTS (Transact-SQL)
Returns the value of the current timestamp data type for the current database.
This timestamp is guaranteed to be unique in the database.
one way is to put that scalar function in the master database and mark it as system object. that way it gets called in the context of the current database
see here for more info:
http://weblogs.sqlteam.com/mladenp/archive/2007/01/18/58287.aspx
Create a stored procedure in your "other" database:
CREATE PROCEDURE dbo.GetDatabaseTimestamp AS
SET NOCOUNT ON;
SELECT ##DBTS AS CurrentRowversion, MIN_ACTIVE_ROWVERSION() AS ActiveRowversion
And then from your current database you can call:
EXECUTE ExternalDB.dbo.GetDatabaseTimestamp;
Thanks for the info, Mladen, that tip is good to know :)
But while that helps me call a function residing in master from in the current database context "ContextCurrent", what I really want is to be able to call the scalar-valued function from the context of the source database "ContextSource".
While in general, I have my reservations against dynamic sql, I ended up using it here as follows.
DECLARE #sourceDbName nvarchar(128)
SET #sourceDbName = N'sbaportia1'
DECLARE #strQuery nvarchar(max)
DECLARE #parmDefn nvarchar(max)
DECLARE #DbtsCapture varbinary(8)
SET #strQuery =
'
N' ' + N'USE' + N' ' + #sourceDbName + N' '
+ N' ' + N'SELECT #dbtsCapture = min_active_rowversion()'
'
SET #parmDefn =
N'
#dbName varchar(128),
#dbtsCapture varbinary(8) OUTPUT
'
EXEC sp_executesql #strQuery
,#parmDefn
,#dbName = 'autobahn'
,#dbtsCapture = #dbtsCapture OUTPUT
SELECT #dbtsCapture
Furthermore, since sp_executesql runs in a separate thread,
the database context within the script below will be
automatically be the same upon the exit of sp_executesql
as upon the entry of sp_executesql.
(I learned waaayy too much about sp_executesql in the early 2000's.)

Resources