2015-12-30

Report SSRS report definition from TSQL

Within this holiday season, it is really difficult to bring on a topic relates to techs. So I decided to make it simple and short: let’s talk about how to manage reports by T-SQL.

If you had read my previous posts, I had demonstrated how to read and modify report definitions by C# codes. But there could be situations that we want to read report definitions for specific purpose, e.g. report documentation.

To document the report definition, try below codes (assume there is a report named "test report"):  

 

USE ReportServer
go

;WITH XMLNAMESPACES   
 (   
 DEFAULT   
 'http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition',   
 'http://schemas.microsoft.com/SQLServer/reporting/reportdesigner'  
 AS rd   
 )  
 SELECT cte.name as ReportName  
   , ds.value('(@Name)[1]', 'VARCHAR(250)') as DatasetName  
      , ds.value('(Query/DataSourceName)[1]','VARCHAR(250)') as DataSourceName  
      , ds.value('(Query/CommandText)[1]','VARCHAR(250)') as Query  
      , fd.value('(@Name)[1]','VARCHAR(250)') as FieldName  
      , fd.value('(DataField)[1]','VARCHAR(250)') as DataField  
      , fd.value('(rd:TypeName)[1]','VARCHAR(250)') as DataType  
  FROM   
  (   
      SELECT C.Name  
           ,CAST(CAST(C.Content as VARBINARY(MAX)) as xml) AS reportXML  
      FROM ReportServer.dbo.Catalog C  
      WHERE C.Content is not null  
      AND C.Type = 2 and c.Path like '%test report%'  
 ) cte  
      CROSS APPLY reportXML.nodes('/Report/DataSets/DataSet') r(ds)  
      CROSS APPLY ds.nodes('Fields/Field') f(fd)   
 ORDER BY name   

It them will give you result like below screenshot:


The only thing needs to be noticed is about the XML namespace declaration. As shown above, the "2010/01" is for SSRS 2012. Depends on the edition of the SSRS you are using, you might need to change the declaration a bit. For instance, if the report was created from SSRS 2008, you need to change the definition to "2008/01".

Simple and easy, isn't it?

So that is it. The last day of 2015, and last post of 2015.  Happy New Year!

2015-12-01

SQL - custom log schema via XML

It has been a long time since my last post: After took my holiday for about one month, it is just so difficult to get myself ready for work :(

But anyway, when I came back immediately I found there were quite a lot works need to be settled: We are changing our whole dialler to a brand new one; we are having some new opportunities from both internal and externals; we are planning to update our data platforms, such as ETL management frameworks, etc, etc.

When I reviewed our team's old works, I found below codes and just want to log here. I could not find the original source of the codes, but if my memory is correct, the credit of the codes should be to one of the SQL books. (professional SQL Server 20xx, probably) So if you know where the source of below codes is, please leave me a message so I can link to codes to original source.

The purpose of below codes is to log DML actions via XML and table triggers. Basically we will turn insert and update actions into xml format through the table trigger, and then log into the DML log table.

Now firstly we have table structure like below

 Create table SysLog.DMLLog  
 (  
 LogKey bigint identity(1,1),  
 TableName varchar(255),  
 TriggerName varchar(255),  
 SpID int,  
 Username varchar(255),  
 DMLAction XML,  
 CreatedOn datetime,  
 Constraint PK_DMLLog primary key(LogKey)  
 )  
 go  

Fields we created are self-explanatory, so I won't spend time to explain it :p.

Now it is time about the trigger, assume we are going to build the trigger on Account table. Something like below:

 Create Trigger TriLogAccountDML   
 On Account  
 After insert, update, delete  
 as  
 BEGIN  
      DECLARE @XmlAction XML, @TriID int, @TblID int  

      SELECT @XmlAction = ISNULL((SELECT * FROM inserted FOR XML AUTO), '<inserted/>') + ISNULL((SELECT * FROM deleted FOR XML AUTO), '<deleted/>') 
 
      SELECT @TriID = @@PROCID  
      SELECT @TblID = parent_id FROM sys.triggers WHERE object_id = @TriID  

      INSERT INTO SysLog.DMLLog(TableName, TriggerName, SpID, Username, DMLAction, CreatedOn)  
      SELECT OBJECT_NAME(@TblID), OBJECT_NAME(@TriID), @@SPID, SYSTEM_USER, @XmlAction, GETDATE();  
 END  

Now we are done. In the end we will have our log table like below:



Quite simple isn't it? :D


2015-09-18

SSIS Replace Email Addresses in Send Mail Task

Lazy Sep! 3 weeks to my holiday!!

Business is changing very quick. Couple months ago, I posted topics such as How to replace images in a SSRS report, and How to replace connection strings in a SSRS report. Now, let me show you how to replace email addresses in the Send Mail Task of the SSIS package.

The scenario is simple, my company's domain changed, so we have to change email's from, to, cc, bcc accounts in quite a lot packages. Within the SSIS package, we have two ways to send an email: by using Send Mail Task, or by calling sp_send_dbmail in Execute SQL task.

The later case is simple, as we usually store email addresses in a helper table. So simply we just need to update email addresses stored in the helper table.

The first case is what I would like to show you, so have a look:

First of the first, we need to add the references into the project. Two specific references we need to add for this task, one is ManagedDTS, and another one is the SendMailTask.






Basically the ManagedDTS gives you a global view of the package, and the SendMailTask gives you specific management capacity for the component. So for example, if you want to replace the file path within the File System Task, you need to add Microsoft.SqlServer.FileSystemTask.

Now in the using section we need to add what we just referenced

After we prepared our project, we can start to 1. load the package, 2. loop the components, 3. do some changes.

One thing needs to notice at step 2, every component can be treated as an Executable in a package scope. But the container component, i.e. For Loop, Foreach Loop, and Sequence containers can contain child executables. Therefore within my code, I need to detect object type of the executable, and then do my change email address task.

To make it simple, I just create a recursive method to loop the executable.

     public void LoopExecutables(Executable e)  
     {  
       if (e is TaskHost)  
       {          
         TaskHost th = (TaskHost)e;  
         if (th.InnerObject is Microsoft.SqlServer.Dts.Tasks.SendMailTask.SendMailTask)  
         {  
           SendMailTask smt = (SendMailTask)th.InnerObject;  
           smt.FromLine = Regex.Replace(smt.FromLine, "<old email address>", "<new email address>", RegexOptions.IgnoreCase);  
           smt.ToLine = Regex.Replace(smt.ToLine, "<old email address>", "<new email address>", RegexOptions.IgnoreCase);  
           smt.CCLine = Regex.Replace(smt.CCLine, "<old email address>", "<new email address>", RegexOptions.IgnoreCase);  
           smt.BCCLine = Regex.Replace(smt.BCCLine, "<old email address>", "<new email address>", RegexOptions.IgnoreCase);  
         }  
       }  
       else if (e is Sequence)  
       {  
         Sequence seq = (Sequence)e;  
         foreach (Executable e2 in seq.Executables)  
         {  
           LoopExecutables(e2);  
         }  
       }  
       else if (e is ForEachLoop)  
       {  
         ForEachLoop fel = (ForEachLoop)e;  
         foreach (Executable e2 in fel.Executables)  
         {  
           LoopExecutables(e2);  
         }  
       }  
       else  
       {  
         MessageBox.Show(e.GetType().FullName);  
       }  
     }  

Now remaining task becomes straightforward. We load the package, we loop the executables in the package, and finally we save the package. Below is the Main() method:


           public void Main()  
           {  
       Microsoft.SqlServer.Dts.Runtime.Application App = new Microsoft.SqlServer.Dts.Runtime.Application();  
       Package package = new Microsoft.SqlServer.Dts.Runtime.Package();  
       string PackagePath = Dts.Variables["strPackagePath"].Value.ToString();  
       string NewPackagePath = Dts.Variables["strNewPackagePath"].Value.ToString();  
       if (File.Exists(PackagePath))  
       {  
         package = App.LoadPackage(PackagePath, null);  
         try  
         {  
           foreach (var item in package.Executables)  
           {              
             LoopExecutables(item as Executable);  
           }  
           string Folder = Path.GetDirectoryName(NewPackagePath);  
           if (!Directory.Exists(Folder))  
           {  
             Directory.CreateDirectory(Folder);  
           }  
           App.SaveToXml(NewPackagePath, package, null);  
         }  
         catch (Exception ex)  
         {  
           MessageBox.Show(ex.Message);  
         }  
       }  
                Dts.TaskResult = (int)ScriptResults.Success;  
           }  

One last thing to mention, if you want to replace the variable value of the package, remember to check variable's namespace, as usually you do not want to touch system variables, like below

           foreach (var item in package.Variables)  
           {  
             Variable var = (Variable)item;  
             if (var.Namespace == "User")  
             {  
               //do something  
             }  
           }