Trigger MSSQL - Output to File (XML) - sql-server

we have a very old ERP system which is badly supported.
Now the warehouse want´s to buy a new "store system" for our goods. It´s a fully automatic store system which need´s data from our ERP system. The support of your ERP system can´t help us, so we have to build a solution of our own.
The idea was to "move" the items for the new storage system to a special storage place called (SHUT1) and output the "part number" and "quantity" to a file (xml) which can be read by the new software.
We can´t change anything in the software of our ERP system, so we have to do it on the SQL Server itself.
(I know, a trigger is not the "best" thing to do, but I have have no other choice)
CREATE TRIGGER tr_LagerShut ON dbo.Lagerverwaltung
AFTER INSERT
AS
BEGIN
IF (SELECT [Lagerort] from Inserted) = 'SHUT1'
BEGIN
DECLARE #Cmd VARCHAR(2000) ;
DECLARE #FormatDate4File VARCHAR(200);
SET #FormatDate4File = (SELECT(SYSUTCDATETIME()));
SET #FormatDate4File = (SELECT REPLACE(#FormatDate4File,' ','-'));
SET #FormatDate4File = (SELECT REPLACE(#FormatDate4File,':','-'));
SET #FormatDate4File = (SELECT REPLACE(#FormatDate4File,'.','-'));
SET #Cmd = ( SELECT [Artikelnummer],[Menge] FROM inserted FOR XML PATH('')) ;
SET #Cmd = 'Echo "' + #Cmd + '" >>"C:\Temp\' + #FormatDate4File +'.xml"' ;
EXEC xp_cmdshell #Cmd ;
END;
END;
The trigger "installs" fine, but if I change a storage place to a new one, the ERP system stalls with "ERROR" (there is no error description :(
If I drop the trigger the system is just running fine again. So I think there is a error in the trigger, but I can´t find it.
Can anybody help please?
Aleks.

Don't know what ERP system stalls with "ERROR" looks like... Frozend GUI? Timeout? Just no file created?
My magic glass bulb tells me the following: You are inserting more than one row at once. If so, this statement will break, because a comparison like this is only valid against a scalar value. If there is more than one row in inserted, this will not work:
IF (SELECT [Lagerort] from Inserted) = 'SHUT1'.
Your trigger can be simplified, but I doubt, that you will like the result. Check this with special characters (like üöä) and check for enclosing "-characters...
CREATE TRIGGER tr_LagerShut ON dbo.Lagerverwaltung
AFTER INSERT
AS
BEGIN
IF EXISTS(SELECT 1 FROM inserted WHERE [Lagerort]='SHUT1')
BEGIN
DECLARE #FileName VARCHAR(255) =REPLACE(REPLACE(REPLACE(SYSUTCDATETIME(),' ','-'),':','-'),'.','-');
DECLARE #Content XML=
(
SELECT [Artikelnummer],[Menge] FROM inserted WHERE [Lagerort]='SHUT1' FOR XML AUTO,ELEMENTS
);
DECLARE #Cmd VARCHAR(4000) = 'Echo "' + CAST(#Content AS VARCHAR(MAX)) + '" >>"c:\temp\' + #FileName + '.xml"' ;
PRINT #cmd;
EXEC xp_cmdshell #Cmd ;
END
END;
This might better be done with BCP.
UPDATE Your comment...
First you should check, if this works at all:
DECLARE #FileName VARCHAR(255) =REPLACE(REPLACE(REPLACE(SYSUTCDATETIME(),' ','-'),':','-'),'.','-');
DECLARE #Content XML=
(
SELECT TOP 5 * FROM sys.objects FOR XML AUTO,ELEMENTS
);
DECLARE #Cmd VARCHAR(4000) = 'Echo "' + CAST(#Content AS VARCHAR(MAX)) + '" >>"c:\temp\' + #FileName + '.xml"' ;
PRINT #cmd;
EXEC xp_cmdshell #Cmd ;
If you find no file in c:\temp\: Are you aware, that SQL-Server will always write in its own context? Might be, that you are awaiting a file in your local c-drive, but the file is written to the Server's machine acutally.
If this works isolatedly, it should work within a trigger too. You might pack the call into BEGIN TRY ... END TRY and add an appropriate CATCH block.

So okay, the "simple" trigger could be really a problem. Now I have this idea:
(one more info: time stamp is not inserted into table "Lagerverwaltung" when a new row is inserted)
Pseudo Code on:
Trigger on Table "Lagerverwaltung"
Check if the storage place(s) is "SHUT1"
If "yes"
DECLARE #FileName VARCHAR(255) =REPLACE(REPLACE(REPLACE(SYSUTCDATETIME(),' ','-'),':','-'),'.','-');
create a new table with name dbo + '.' + #filename
Insert all the data (inserted) + row(SYSUTCDATETIME()) AS TimeStamp where 'Lagerort' = 'SHUT1' into new table named 'dbo.' + #filename
DECLARE #Cmd varchar(4000) = 'bcp "select [Artikelnummer],[Menge],[TimeStamp] FROM [wwsbautest].[dbo].#filename WHERE [Lagerort]=''SHUT1'' AND [Menge] > ''0'' FOR XML AUTO, ELEMENTS" queryout "C:\temp\' + #FileName + '.xml" -c -T';
EXEC xp_cmdshell #Cmd;
Drop table dbo.#filename;
Could somthing like that work?

Related

Could not find stored procedure 'sp_msforeachtable' while looping through server for stats (SSMS)

I've written this to loop through each database on a server, collecting the statistics for each table and storing them in a temp table. Eventually, I'll integrate this into a more permanent structure, but for now I'm just trying to get this working. My problem is, after 57 databases, I get the error stating it can't find the stored procedure sp_msforeachtable.
I've verified that this stored procedure exists on every database on the server and on the server level.
I've excluded this database in the findings by adding it to the "where name not in" condition, and it just moves to the next one in the list and gives the same error.(I've confirmed it exists on the next database also). I've actually done this for the next 6 databases.
This is causing me to not collect accurate information. Am I running out of resources somewhere?
DECLARE #Database TABLE (DbName SYSNAME);
IF OBJECT_ID('tempdb.dbo.#TableLvlSizes', 'U') IS NOT NULL
BEGIN
PRINT 'dropping table'
DROP TABLE tempdb.dbo.#TableLvlSizes;
END
CREATE TABLE #TableLvlSizes (
TableName nvarchar(128)
,NumberOfRows varchar(50)
,ReservedSpace varchar(50)
,TableDataSpace varchar(50)
,IndexSize varchar(50)
,unused varchar(50))
DECLARE #DbName AS SYSNAME;
DECLARE #Sql1 AS VARCHAR(MAX);
SET #DbName = '';
INSERT INTO #Database (DbName)
SELECT NAME
FROM sys.databases
where name not in ('tempdb')
ORDER BY NAME ASC;
WHILE #DbName IS NOT NULL
BEGIN
SET #DbName = (
SELECT MIN(DbName)
FROM #Database
WHERE DbName > #DbName
);
print #DbName;
SET #Sql1 =
'USE ' + #DbName + '; ' + '
Exec sp_msforeachtable
''insert into #TableLvlSizes exec sp_spaceused [?]''
'
Exec (#SQL1);
END
If someone is using Azure SQL, they will not find sp_MSforeachtable since it is not available in Azure SQL.
You may need to create one for yourself.
Since you already verified that the stored procedure does in fact exist, I believe your database is case sensitive. Therefore, the error is still accurate. Basically, the stored procedure with the case you used does not exist. The actual procedure name is sp_MSforeachtable
In your code, you are using the following:
Exec sp_msforeachtable
If you change your code to use the proper case for the stored procedure to be sp_MSforeachtable, it should work:
SET #Sql1 =
'USE ' + #DbName + '; ' + '
Exec sp_MSforeachtable
''insert into #TableLvlSizes exec sp_spaceused [?]'''

How can i use "Use [database]" transactions within IF statements in SQL?

I'm trying to do something along the lines of:
IF(##SERVERNAME = 'SERVER1')
BEGIN
USE Appt
END
IF(##SERVERNAME = 'SERVER2')
BEGIN
USE ApptDEMO
END
At work, our database for production is "Appt" but the one for test environment is "ApptDEMO."
They're the same thing, but they're just named differently.
It runs fine in the test environment because both "Appt" and "ApptDEMO" exist there (it just doesnt use "Appt").
But in production, it tells me that "ApptDEMO" doesn't exist.
I want to create a script that I don't need to make x amount of different scripts for different environments.
Is this possible?
Try this (with dynamic SQL):
DECLARE #sql nvarchar(4000)
IF (##SERVERNAME = 'SERVER1')
BEGIN
SET #sql = N'USE Appt'
END
ELSE IF (##SERVERNAME = 'SERVER2')
BEGIN
SET #sql = N'USE ApptDEMO'
END
IF (#sql != N'')
BEGIN
EXECUTE sp_executesql #sql
END
Database can't be changed dynamically (USE [DB]) other then making the query dynamic.
Below code may clarify your understanding.
SELECT DB_NAME();
-- Master --Default Database
SELECT ##SERVERNAME
-- SERVER1
DECLARE #USEDB NVARCHAR(MAX) =
CASE ##SERVERNAME WHEN 'SERVER1' THEN 'USE [Appt];'
WHEN 'SERVER2' THEN 'USE [ApptDEMO];'
END -- Same as IF statement
EXEC(#USEDB) -- The database [Appt] will be changed within this batch, not outside.
SELECT DB_NAME();
-- Master --Default Database
DECLARE #MyQuery VARCHAR(MAX) = 'Select DB_NAME();'
DECLARE #UseDBWithQuery VARCHAR(MAX) = #USEDB + #MyQuery
EXEC(#UseDBWithQuery)
-- Appt

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.

Exporting XML Data to mutiple files but retain structure

I have been searching on the net for the last couple of days and unfortunately cannot find quite what I am looking for. I have a SQL Server 2012 table that contains 2 columns, one is PurchaseOrder DataType varchar(10) the other is Data with a DataType of XML. Currently what is happening ever few hours a stored procedure is run to check a number of table for a value and then create a PurchaseOrder in the desired XML format. This has been achieved and validated by the company that I am going to be sending the data to. The issue I now have is I want to create a SSIS package that looks at this table and creates multiple xml files with the contents of the Data field "not putting the contents in to one long line" and then using the PurchaseOrder field for the file name. I have found various part solutions e.g. using a Script task as there is no native XML Destination in SSIS but struggling and being quite new to SSIS not getting anywhere fast. Can anyone offer any help, guidance, youtube links or even a part way there package.
Thanks P
Further to the above question I have tried to go about it using the bcp command, below is the following code that works great just looking at the top 1 record
DECLARE #fileName VARCHAR(50)
DECLARE #sqlStr VARCHAR(1000)
DECLARE #sqlCmd VARCHAR(1000)
SET #fileName = 'C:\Temp\test.xml'
SET #sqlStr = 'select TOP 1 Data from DatabaseName.dbo.OutputXML'
SET #sqlCmd = 'bcp "' + #sqlStr + '" queryout ' + #fileName + '
-w -T -S Server\Instance'
EXEC xp_cmdshell #sqlCmd
What I have then done is add this in to a cursor, which again works to a point, it creates my files, using the purchaseorder as the file name and outputs the relevant information but it no longer holds the proper xml format, it goes back to looking like a string with none of the original formatting, is this because I am imbedding it in the cursor?? Below is the code that I am using
DECLARE #fileName VARCHAR(50)
DECLARE #sqlStr VARCHAR(1000)
DECLARE #sqlCmd VARCHAR(1000)
DECLARE xml_cursor CURSOR FOR
SELECT PurchaseOrder from DatabaseName.dbo.OutputXML
OPEN xml_cursor
FETCH NEXT FROM xml_cursor INTO #fileName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #fileName = 'C:\Temp\'+#fileName+'.xml'
SET #sqlStr = 'select Data from DatabaseName.dbo.OutputXML'
SET #sqlCmd = 'bcp "' + #sqlStr + '" queryout ' + #fileName + '
-w -T -S Server\Instance'
EXEC xp_cmdshell #sqlCmd
FETCH NEXT FROM xml_cursor into #fileName
END
CLOSE xml_cursor
DEALLOCATE xml_cursor
Since bcp is a command line utility it does not know about your cursor. You can only change the parameters passed. You already pass the #filename from the cursor, you will also need to filter your query. Below is how to fix your problem but it is far from a great solution, it will be quite slow. I don't know what your key is called, so I called it SomeKey
DECLARE #fileName VARCHAR(50)
DECLARE #sqlStr VARCHAR(1000)
DECLARE #sqlCmd VARCHAR(1000)
DECLARE #SomeKey VARCHAR(100) --Put your key datatype here
DECLARE xml_cursor CURSOR FOR
-- replace SomeKey with your key
SELECT SomeKey,PurchaseOrder FROM DatabaseName.dbo.OutputXML
OPEN xml_cursor
FETCH NEXT FROM xml_cursor INTO #SomeKey,#fileName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #fileName = 'C:\Temp\' + #filename + '.xml'
SET #sqlStr = 'SELECT Data FROM DatabaseName.dbo.OutputXML WHERE SomeKey = ''' + #SomeKey +''''
SET #sqlCmd = 'bcp "' + #sqlStr + '" queryout ' + #fileName + '-w -T -S Server\Instance'
EXEC xp_cmdshell #sqlCmd
FETCH NEXT FROM xml_cursor into #SomeKey,#fileName
END
CLOSE xml_cursor
DEALLOCATE xml_cursor

sql use statement with variable

I'm trying to switch the current database with a SQL statement.
I have tried the following, but all attempts failed:
USE #DatabaseName
EXEC sp_sqlexec #Sql -- where #Sql = 'USE [' + #DatabaseName + ']'
To add a little more detail.
EDIT: I would like to perform several things on two separate database, where both are configured with a variable. Something like this:
USE Database1
SELECT * FROM Table1
USE Database2
SELECT * FROM Table2
The problem with the former is that what you're doing is USE 'myDB' rather than USE myDB.
you're passing a string; but USE is looking for an explicit reference.
The latter example works for me.
declare #sql varchar(20)
select #sql = 'USE myDb'
EXEC sp_sqlexec #Sql
-- also works
select #sql = 'USE [myDb]'
EXEC sp_sqlexec #Sql
exec sp_execsql #Sql
The DB change only lasts for the time to complete #sql
http://blog.sqlauthority.com/2007/07/02/sql-server-2005-comparison-sp_executesql-vs-executeexec/
I have the same problem, I overcame it with an ugly -- but useful -- set of GOTOs.
The reason I call the "script runner" before everything is that I want to hide the complexity and ugly approach from any developer that just wants to work with the actual script. At the same time, I can make sure that the script is run in the two (extensible to three and more) databases in the exact same way.
GOTO ScriptRunner
ScriptExecutes:
--------------------ACTUAL SCRIPT--------------------
-------- Will be executed in DB1 and in DB2 ---------
--TODO: Your script right here
------------------ACTUAL SCRIPT ENDS-----------------
GOTO ScriptReturns
ScriptRunner:
USE DB1
GOTO ScriptExecutes
ScriptReturns:
IF (db_name() = 'DB1')
BEGIN
USE DB2
GOTO ScriptExecutes
END
With this approach you get to keep your variables and SQL Server does not freak out if you happen to go over a DECLARE statement twice.
Just wanted to thank KM for his valuable solution.
I implemented it myself to reduce the amount of lines in a shrinkdatabase request on SQLServer.
Here is my SQL request if it can help anyone :
-- Declare the variable to be used
DECLARE #Query varchar (1000)
DECLARE #MyDBN varchar(11);
-- Initializing the #MyDBN variable (possible values : db1, db2, db3, ...)
SET #MyDBN = 'db1';
-- Creating the request to execute
SET #Query='use '+ #MyDBN +'; ALTER DATABASE '+ #MyDBN +' SET RECOVERY SIMPLE WITH NO_WAIT; DBCC SHRINKDATABASE ('+ #MyDBN +', 1, TRUNCATEONLY); ALTER DATABASE '+ #MyDBN +' SET RECOVERY FULL WITH NO_WAIT'
--
EXEC (#Query)
try this:
DECLARE #Query varchar(1000)
DECLARE #DatabaseName varchar(500)
SET #DatabaseName='xyz'
SET #Query='SELECT * FROM Server.'+#DatabaseName+'.Owner.Table1'
EXEC (#Query)
SET #DatabaseName='abc'
SET #Query='SELECT * FROM Server.'+#DatabaseName+'.Owner.Table2'
EXEC (#Query)
I case that someone need a solution for this, this is one:
if you use a dynamic USE statement all your query need to be dynamic, because it need to be everything in the same context.
You can try with SYNONYM, is basically an ALIAS to a specific Table, this SYNONYM is inserted into the sys.synonyms table so you have access to it from any context
Look this static statement:
CREATE SYNONYM MASTER_SCHEMACOLUMNS FOR Master.INFORMATION_SCHEMA.COLUMNS
SELECT * FROM MASTER_SCHEMACOLUMNS
Now dynamic:
DECLARE #SQL VARCHAR(200)
DECLARE #CATALOG VARCHAR(200) = 'Master'
IF EXISTS(SELECT * FROM sys.synonyms s WHERE s.name = 'CURRENT_SCHEMACOLUMNS')
BEGIN
DROP SYNONYM CURRENT_SCHEMACOLUMNS
END
SELECT #SQL = 'CREATE SYNONYM CURRENT_SCHEMACOLUMNS FOR '+ #CATALOG +'.INFORMATION_SCHEMA.COLUMNS';
EXEC sp_sqlexec #SQL
--Your not dynamic Code
SELECT * FROM CURRENT_SCHEMACOLUMNS
Now just change the value of #CATALOG and you will be able to list the same table but from different catalog.
If SQLCMD is an option, it supports scripting variables above and beyond what straight T-SQL can do. For example: http://msdn.microsoft.com/en-us/library/ms188714.aspx
You can do this:
Declare #dbName nvarchar(max);
SET #dbName = 'TESTDB';
Declare #SQL nvarchar(max);
select #SQL = 'USE ' + #dbName +'; {can put command(s) here}';
EXEC (#SQL);
{but not here!}
This means you can do a recursive select like the following:
Declare #dbName nvarchar(max);
SET #dbName = 'TESTDB';
Declare #SQL nvarchar(max);
SELECT #SQL = 'USE ' + #dbName + '; ' +(Select ... {query here}
For XML Path(''),Type)
.value('text()[1]','nvarchar(max)');
Exec (#SQL)
Use exec sp_execsql #Sql
Example
DECLARE #sql as nvarchar(100)
DECLARE #paraDOB datetime
SET #paraDOB = '1/1/1981'
SET #sql=N'SELECT * FROM EmpMast WHERE DOB >= #paraDOB'
exec sp_executesql #sql,N'#paraDOB datetime',#paraDOB
-- If you are using a variable for the database name.
-- Try something like this.
DECLARE #DBName varchar(50)
Set #DBName = 'Database1'; /* could be passed in by a parameter. */
IF( #DBName = 'Database1')
Begin
USE [Database1];
SELECT FROM Table1;
End
IF( #DBName = 'Database2')
Begin
USE [Database2];
SELECT FROM Table2;
End
IF( #DBName is null)
Begin
USE [Database1];
End

Resources