A blog about SQL Server, SSIS, C# and whatever else I happen to be dealing with in my professional life.

Find ramblings

Friday, March 23, 2012

Replicate tables with SSIS EzAPI

This post is out of order for the recipe series but there was a need on dba.stackexchange.com for this solution. Add a new class to the solution named ReplicateOMatic. This class will create a new package with a pair of connection managers and configure a data flow to copy all of the between paired tables. Modify the query in the method GenerateTableList to create the correct list of tables.
namespace EzAPIRecipies
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.SqlServer.SSIS.EzAPI;
    using Microsoft.SqlServer.Dts.Runtime;

    public class ReplicateOMatic : EzSrcDestPackage<EzOleDbSource, EzSqlOleDbCM, EzOleDbDestination, EzSqlOleDbCM>
    {
        public static string connectionStringTemplate = @"Data Source={0};Initial Catalog={1};Provider=SQLNCLI10.1;Integrated Security=SSPI;Packet Size=32767;";

        public ReplicateOMatic(Package p) : base(p) { }

        public static implicit operator ReplicateOMatic(Package p) { return new ReplicateOMatic(p); }
        
        /// <summary>
        /// Create a package with a single data flow, 2 variables.
        /// The data flow will contain an OLEDB source and destination component.
        /// We will use fastload options, identity inserts, table locking etc.
        /// </summary>
        /// <param name="sourceServer">Source computer name (UTUMNO)</param>
        /// <param name="sourceDatabase">Source catalog name (DocumentsOld)</param>
        /// <param name="table">Table to be replicated [schema].[tablename]</param>
        /// <param name="destinationServer">Destination computer name (WESTMARCH)</param>
        /// <param name="destinationDatabase">Destination catalog name (Documents)</param>
        public ReplicateOMatic(string sourceServer, string sourceDatabase, string table, string destinationServer, string destinationDatabase)
            : base()
        {
            string saniName = TableToTable.SanitizeName(table);
            string sourceQuery = string.Format("SELECT D.* FROM {0} D", table);

            ////////////////////////////////////////////////////
            // Pacakge settings
            ////////////////////////////////////////////////////
            this.Name = string.Format("Replicate{0}", saniName);

            /////////////////////////////////////////////////////
            // Define package variables
            /////////////////////////////////////////////////////
            this.Variables.Add("sourceQuery", false, "User", sourceQuery);
            this.Variables.Add("tableName", false, "User", table);

            /////////////////////////////////////////////////////
            // Configure DataFlow properties
            /////////////////////////////////////////////////////
            this.DataFlow.Name = string.Format("DFT_{0}", saniName);
            this.DataFlow.Description = "Scripted replication";

            /////////////////////////////////////////////////////
            // Connection manager configuration
            /////////////////////////////////////////////////////
            this.SrcConn.ConnectionString = string.Format(ReplicateOMatic.connectionStringTemplate, sourceServer, sourceDatabase);
            this.SrcConn.Name = string.Format("SRC_{0}", sourceDatabase);
            this.SrcConn.Description = string.Empty;

            this.DestConn.ConnectionString = string.Format(ReplicateOMatic.connectionStringTemplate, destinationServer, destinationDatabase);
            this.DestConn.Name = string.Format("DST_{0}", destinationDatabase);
            this.DestConn.Description = string.Empty;

            /////////////////////////////////////////////////////
            // Configure Dataflow's Source properties
            /////////////////////////////////////////////////////
            this.Source.Name = "Src " + saniName;
            this.Source.Description = string.Empty;
            this.Source.AccessMode = AccessMode.AM_SQLCOMMAND;
            this.Source.SqlCommand = sourceQuery;

            /////////////////////////////////////////////////////
            // Configure Dataflow's Destination properties
            /////////////////////////////////////////////////////
            this.Dest.Name = "Dest " + saniName;
            this.Dest.Description = string.Empty;
            this.Dest.Table = table;
            this.Dest.FastLoadKeepIdentity = true;
            this.Dest.FastLoadKeepNulls = true;
            this.Dest.FastLoadOptions = "TABLOCK,CHECK_CONSTRAINTS";
            this.Dest.AccessMode = AccessMode.AM_OPENROWSET_FASTLOAD_VARIABLE;
            this.Dest.DataSourceVariable = this.Variables["tableName"].QualifiedName;

            this.Dest.LinkAllInputsToOutputs();
        }

        /// <summary>
        /// Generate a list of all the tables in schema foo or bar
        /// </summary>
        /// <returns>Returns a list of all tables matching the query criteria. Format is [schema].[table]</returns>
        public static List<string> GenerateTableList(string server, string catalog)
        {
            List<string> tables = null;
            using (System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection(string.Format("server={0};integrated security=sspi;database={1};", server, catalog)))
            {
                connection.Open();
                string sql = @"
                    SELECT
                        quotename(schema_name(T.schema_id)) + '.' + quotename(T.name)
                    FROM
                        sys.tables T
                    WHERE
                        T.schema_id = schema_id('foo')
                        OR T.schema_id = schema_id('bar')
                    ORDER BY
                    1";
                using (System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand(sql, connection))
                {
                    command.CommandType = System.Data.CommandType.Text;
                    using (System.Data.SqlClient.SqlDataReader reader = command.ExecuteReader())
                    {
                        tables = new List<string>();
                        while (reader.Read())
                        {
                            tables.Add(reader[0].ToString());
                        }
                    }
                }
            }
            return tables;
        }

        /// <summary>
        /// Sanitize a name so that it is valid for SSIS objects. 
        /// Strips []/\:=
        /// Replaces . with _
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static string SanitizeName(string name)
        {
            string saniName = name.Replace("[", String.Empty).Replace("]", string.Empty).Replace(".", "_").Replace("/", string.Empty).Replace("\\", string.Empty).Replace(":", string.Empty);
            return saniName;
        }
    }
}

Usage

This code would be added to the original program. Call it from the Main and modify the code to reflect the correct server and catalog names
        /// <summary>
        /// Build a set of SSIS packages that take the form of ReplicateSchema_Table.dtsx
        /// </summary>
        public static void ReplicateTables()
        {
            string outputFolder = @"C:\sandbox\SSISHackAndSlash\SSISHackAndSlash";
            string sourceServer = @"ANGBAND";
            string sourceDatabase = "OldDocuments";
            string destinationServer = @"UTUMNO";
            string destinationDatabase = "Documents";
            string outputFile = string.Empty;
            bool tweak = true;
            
            foreach (string table in ReplicateOMatic.GenerateTableList(sourceServer, sourceDatabase))
            {
                ReplicateOMatic rom = new ReplicateOMatic(sourceServer, sourceDatabase, table, destinationServer, destinationDatabase);
                outputFile = System.IO.Path.Combine(outputFolder, string.Format("{0}.dtsx", rom.Name));
                // This section is optional but I like to tweak things
                if (tweak)
                {
                    Package p = null;
                    Application app = null;
                    app = new Application();
                    p = new Package();

                    // Connection managers are easier through EzAPI
                    EzSqlOleDbCM loggingCM = new EzSqlOleDbCM(p);
                    string loggingCatalog = string.Empty;
                    //loggingCatalog = "SSISDB";
                    loggingCatalog = "SRC_msdb"; 
                    loggingCM.Name = loggingCatalog;
                    loggingCM.ConnectionString = string.Format(ReplicateOMatic.connectionStringTemplate, sourceServer, loggingCatalog);

                    // Serialize the EzAPI to XML and load our package from it
                    p.LoadFromXML(rom.SaveToXML(), null);

                    ///////////////////////////////////////////////////////////////
                    // Enable SQL Server configuration, assumes 
                    // an OLE DB connection manager named SSISDB exists
                    // uses a table called dbo.sysdtsconfig
                    // Configuration key is Default.2008.SalesDB
                    ///////////////////////////////////////////////////////////////
                    //p.EnableConfigurations = true;
                    //Configuration c = null;
                    //c = p.Configurations.Add();
                    //c.Name = "SalesDB";
                    //c.ConfigurationType = DTSConfigurationType.SqlServer;
                    //c.ConfigurationString = @"""SSISDB"";""[dbo].[sysdtsconfig]"";""Default.2008.SalesDB""";

                    ///////////////////////////////////////////////////////////////
                    // Enable logging
                    // Add log provider and such
                    // http://msdn.microsoft.com/en-us/library/microsoft.sqlserver.dts.runtime.logproviders.add.aspx
                    // ProgID: DTS.LogProviderSQLServer.2
                    ///////////////////////////////////////////////////////////////
                    DTSEventColumnFilter eventFilter = new DTSEventColumnFilter();
                    eventFilter.Computer = true;
                    eventFilter.DataBytes = true;
                    eventFilter.ExecutionID = true;
                    eventFilter.MessageText = true;
                    eventFilter.Operator = true;
                    eventFilter.SourceID = true;
                    eventFilter.SourceName = true;

                    p.LoggingMode = DTSLoggingMode.Enabled;

                    // Define the events we care about
                    string[] notableEvents = new string[] { "OnError", "OnInformation", "OnPostExecute", "OnPreExecute", "OnWarning", "OnTaskFailed" };

                    // Add and configure the sql log provider
                    LogProvider provider = p.LogProviders.Add("DTS.LogProviderSQLServer.2");

                    provider.ConfigString = loggingCM.Name;
                    provider.Name = "SSIS log provider for SQL Server";
                    provider.Description = "Writes log entries for events to a SQL Server database";
                    p.LoggingOptions.SelectedLogProviders.Add(provider);

                    LoggingOptions options = p.LoggingOptions;
                    // load up the events we care about
                    options.EventFilter = notableEvents;
                    options.EventFilterKind = DTSEventFilterKind.Inclusion;

                    // configure the specifics of how an event should be logged
                    foreach (string item in notableEvents)
                    {
                        options.SetColumnFilter(item, eventFilter);
                    }
                    app.SaveToXml(outputFile, p, null);
                }
                else
                {
                    rom.SaveToFile(outputFile);
                }
            }
        }

Friday, March 16, 2012

Recover CLR assemblies from SQL Server

This is an amazingly short post covering how to get save a CLR assembly out of SQL Server via PowerShell. Original code was on a stackoverflow question I don't have reference to at the moment. Run as is, this code will extract all the user assemblies in a given database catalog and save them out to the C:\ssisdata folder. If you know the particular assembly you want, you can uncomment lines 17 (tsql), 32 (PowerShell) and update line 50 to point to the correct assembly name. Run the query template if you don't know all the assembly names in a given database. And obviously, fix line 48 to point to the correct database.
   1:  #[Reflection.Assembly]::LoadWithPartialName("Microsoft.SQLServer.ManagedDTS") | out-null
   2:   
   3:  $assemblyListQuery = "SELECT A.name FROM sys.assemblies A WHERE A.is_user_defined = 1 ORDER BY 1 ASC "
   4:   
   5:  $queryTemplate = "
   6:  SELECT 
   7:      AF.content
   8:  ,    A.name    
   9:  FROM 
  10:      sys.assembly_files AF 
  11:      INNER JOIN 
  12:          sys.assemblies A 
  13:          ON AF.assembly_id = A.assembly_id 
  14:  WHERE 
  15:      AF.file_id = 1
  16:      AND A.is_user_defined = 1
  17:      --AND A.name = @assemblyName
  18:  "
  19:   
  20:  Function Save-Assemblies()
  21:  {
  22:      param
  23:      (
  24:           [string]$connectionString
  25:      ,   [string]$query
  26:      )
  27:          $connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
  28:          $command = New-Object System.Data.SqlClient.SqlCommand($query)
  29:          
  30:          $connection.Open()
  31:          $command.Connection = $connection
  32:          #$hush = $command.Parameters.AddWithValue("@assemblyName", $assemblyName)
  33:          $reader = $command.ExecuteReader()
  34:          
  35:          while ($reader.Read())
  36:          {
  37:              #$bytes = New-Object System.Data.SqlClient.SqlBytes()
  38:              $bytes = $reader.GetSqlBytes(0)
  39:              $name = $reader.GetString(1)
  40:              $bytestream = New-Object System.IO.FileStream("C:\ssisdata\$name.dll", [System.IO.FileMode]::Create)
  41:              $bytestream.Write($bytes.Value, 0, $bytes.Length)
  42:              $bytestream.Close()
  43:          }
  44:              
  45:  }
  46:   
  47:   
  48:  $bnode = "Data Source=localhost;Initial Catalog=FOO;Integrated Security=SSPI;"
  49:   
  50:  $assemblyName = "NiftyAssembly"
  51:      
  52:  Save-Assemblies $bnode $queryTemplate

Now that you have the assembly, use a tool like Reflector or ILSpy to recover as much of the source code as you can. Happy decompiling.

Tuesday, March 13, 2012

T-SQL Tuesday #028 Specialist vs Generalist

For me, this is a perception versus reality discussion. Coworkers generally assume I'm a SQL Server specialist, maybe even they think I'm more specialized than that and I only care about SSIS. Au vibrator (I'm blogging this on my phone and swype does not recognize contraire and corrects to "vibrator." It's funny, laugh), what I love is problem solving and I am pretty good at using a computer to solve problems.

In college, Professor Walters said if you don't like reading and aren't interested in life long learning, you were in the wrong field. He was right. I graduated with a degree in computer science and the ability to write the most amazing c++ code known to man. I actually did think I was good at it but looking back, I'd have had lunch handed to me had I ever interviewed for a c++ position. Instead, I landed a job working on an erp system with a c-like language. What a horrible, misguided year and a half that was, but I learned some database stuff (ingres) and a 4gl for that position.
Hating what I did provided the impetus to take a course on java. That lead to creating a monitoring app (production java experience) and I lucked into maintaining a new asp site. I wrote html by hand and vb was not hard to figure out so I left that job with plenty of things I wasn't planning on having experience with (I've had a recruiter ask about the postscript experience I pickler picked up there. Stack based languages are cool but omfg what a mind scrambler).

Since then, I've learned so much. Some of it is applicable daily while other bits will probably never be ever again useful. For the past few years, I've really focused on sql server for three reasons:  I enjoy it, the pay is good and you, my #sqlfamily. More than anything else, it's the later that keeps me here. But, not to the detriment of acquiring a wide breadth of knowledge. Presenting at the user group and SQL Saturdays has really opened up a new avenue of learning for me, all those soft skills! That's going to apply well beyond when SQL Server is nothing but a fond memory. Making reports is easy but making good reports, that's hard. There's a whole field of information design and data visualization that I need to learn to be able to effectively deliver information. I may not like making GUIs but a good report is a little slice of heaven.

The pattern of learning whatever it takes to solve a problem is invaluable to remaining relevant in today's job market. Specialize all you want, just make sure the focus is on the process of learning and not the technology, because that's going to change.