Friday, 15 June 2012

T-SQL: Scripting tables and Named constraints

In a number of projects that I am currently working on I have had to make schema changes.  When creating these schema changes we generally create changes scripts that get run as part of a version upgrade.  The change scripts will generally be applied either on application startup or by a command line application that will run any new upgrade scripts and update a version number in that database.  To get an idea of how this can be done have a look at how FluentMigrator works.

I don't particularly like creating new tables in SQL Server Management Studio and then using the script generator to create a create table script as I find the resulting scripts are always pretty verbose.
Writing a create table script is pretty straight forward.  Here is an example:

CREATE TABLE [dbo].[Employee]
(
  EmployeeID int not null PRIMARY KEY,
  FirstName varchar(100) not null,
  LastName varchar(100) not null
)

The problem with this script is that the primary key is not explicitly named.  So how do we go about explicitly naming the primary key constraint and for that matter any other constraint.  Here is an example that demonstrates how to do this for primary keys, foreign keys and unique constrains:

CREATE TABLE [dbo].[JobTitle]
(
 JobTitleID int not null 
  CONSTRAINT [PK_JobTitle] PRIMARY KEY (JobTitleID),
 Name varchar(100) not null,
 [Description] varchar(2000) null
)

CREATE TABLE [dbo].[Employee]
(
  EmployeeID int not null 
 CONSTRAINT [PK_Employee] PRIMARY KEY (EmployeeID),
  FirstName varchar(100) not null,
  LastName varchar(100) not null,
  IDNumber varchar(30) not null 
 CONSTRAINT [UC_IDNumber] UNIQUE (IDNumber),
  JobTitleID int null 
 CONSTRAINT [FK_Employee_JobTitle] REFERENCES [dbo].[JobTitle]
)

One thing that is not demonstrated in this code snippet is how to add a second named constraint onto a field.  It is pretty straight forward.  I have specifically layed out the example with the constraints indented on the line below the field they relate to.  Notice that there is no comma before the CONSTRAINT keyword.  To add another constraint that applies to the same field, you just add another constraint keyword and continue.  I have indented the constraints and put them on their own line so that the code is more readable in the case where there are multiple constraints on any one field.

Friday, 1 June 2012

SQLCMD and BACKUP - watch out for this

Yesterday, I was working creating a rake task for backing up a database.  As I am still pretty new to ruby I decided to see if I could get the backup code working with sqlcmd from the command line first.  I created a backupdb.sql file to do the backup and then called it from the command line.

This is what the backupdb.sql looked like:

BACKUP DATABASE $(database)  
    TO DISK = $(backupfilename)

Not much to it!
I then executed the following sqlcmd command:

sqlcmd -S  localhost -i BackupDb.sql -v database="mydb" -v backupfilename="mydb.bak"

When I just specified the database and backupfilename the backup worked fine with the backup file being written to the default SQL Server backup folder.  I then changed the backupfilename to include a path as I wanted to specify where the backup file should be saved. This is the command I executed:

sqlcmd -S  localhost -i BackupDb.sql -v database="mydb" -v backupfilename="E:\sqlbackups\mydb.bak"

This time I got the following error:
Msg 102, Level 15, State 1, Server ANDREW-PC, Line 3 Incorrect syntax near 'E:'.
After a lot of fiddling around I figured out what I needed to do to fix the problem. The problem was that the backupfilename needs to be unicode.  See below for the fixed backupdb.sql

BACKUP DATABASE $(database)  
    TO DISK = N'$(backupfilename)'

Thursday, 24 May 2012

SQL Server - Query to get table sizes

Once again I am posting SQL that I have found on the internet (mainly so that I have can find this scriopt easily in the future).  This one is thanks to marc_s and can be found in this Stackoverflow post.  The script will give you a breakdown of the table size for each table in your database.  This includes a row count, disk space used and disk space available.  I have modified the original script slightly as a number of databases I work with have multiple schemas and the original script doesn't show the schema name.

SELECT
    s.NAME AS SchemaName,  
    t.NAME AS TableName, 
    p.rows AS RowCounts, 
    SUM(a.total_pages) * 8 AS TotalSpaceKB,  
    SUM(a.used_pages) * 8 AS UsedSpaceKB,  
    (SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB 
FROM  sys.tables t 
INNER JOIN sys.Schemas s ON t.schema_id = s.schema_id
INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id 
INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id 
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id 
WHERE  
    t.NAME NOT LIKE 'dt%'  
    AND t.is_ms_shipped = 0 
    AND i.OBJECT_ID > 255  
GROUP BY
 s.Name,  
    t.Name, 
    p.[Rows] 
ORDER BY  
 s.Name,
    t.Name 

Wednesday, 16 May 2012

T-SQL script for finding object dependencies

This morning I wanted to find out where a specific stored procedure was being used.  I wasn't sure how to do this using T-SQL so did a bit of googling and found this article.  I have made a slight enhancement to the scripts shown in the article. 

DECLARE @ObjectName VARCHAR(500)
SET @ObjectName = 'dbo.sp_test_Proc'

-- shows objects that the named object depends on
SELECT DISTINCT OBJECT_NAME(DEPID) DEPENDENT_ON_OBJECT, OBJECT_NAME (ID) OBJECTNAME
FROM SYS.SYSDEPENDS
WHERE ID = OBJECT_ID(@ObjectName)

-- shows objects that use the named object
SELECT DISTINCT  OBJECT_NAME(DEPID) OBJECT_NAME, OBJECT_NAME (ID) USED_IN_OBJECT
FROM SYS.SYSDEPENDS
WHERE DEPID = OBJECT_ID(@ObjectName)

I am sure I will get a lot of milage out of this in the future.

Monday, 30 April 2012

tSQLt - Assertions

In this post my aim is to introduce the basic tSQLt assertions.  The examples should be enough to get you going with assertions. My assumption is that you have at least some experience writing unit tests with any unit testing framework.

tSQLt.Fail
CREATE PROCEDURE [UnitTest_FirstGo].[test Fail]
AS
BEGIN
  EXEC tSQLt.Fail 'This test should fail and display this message';
END
 tSQLt.AssertEquals

CREATE PROCEDURE [UnitTest_FirstGo].[test AssertEquals]
AS
BEGIN
  --Assemble
  DECLARE @expected int
  DECLARE @actual int
 SET @expected = 1;
 SET @actual = 0;
  
  --Act
  
  --Assert
  EXEC tSQLt.AssertEquals @expected, @actual, 'The is a TEST FAILURE MESSAGE!'
END
 tSQLt.AssertEquals with System Under Test (SUT)

First we need to create something to represent the SUT
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[fn_SUM]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[fn_SUM]
 
GO

CREATE FUNCTION fn_SUM
(
 @a int,
 @b int
)
RETURNS int
BEGIN
 RETURN @a+@b;
END
and now the test
CREATE PROCEDURE [UnitTest_FirstGo].[test AssertEquals with SUT]
AS
BEGIN
  --Assemble
  DECLARE @expected int
  DECLARE @actual int
 SET @expected = 3;
  --Act
 SELECT @actual = dbo.fn_SUM(1,2);
  
  --Assert
  EXEC tSQLt.AssertEquals @expected, @actual
END
tSQLt.AssertEqualsString
CREATE PROCEDURE [UnitTest_FirstGo].[test AssertEqualsString]
AS
BEGIN
  --Assemble
  DECLARE @expected varchar(20)
  DECLARE @actual varchar(20)
 SET @expected = 'hello world';
 SET @actual = 'goodbye world';
  
  --Act
  
  --Assert
  EXEC tSQLt.AssertEqualsString @expected, @actual, 'The is a TEST FAILURE MESSAGE!'

END

tSQLt.AssertEqualsTable

This one takes a bit of time to get your head around, but once you get going and when used in conjunction with Fake tables it's really awesome.
CREATE PROCEDURE [UnitTest_FirstGo].[test AssertEqualsTable]
AS
BEGIN
  --Assemble
 if exists(select * 
 from information_schema.tables
 where table_schema='dbo' and table_name='tablea')
 begin
  drop table dbo.TableA;
  create table dbo.TableA (id int not null);
 end

 if exists(select * 
 from information_schema.tables
 where table_schema='dbo' and table_name='tableb')
 begin
  drop table dbo.TableB;
  create table dbo.TableB (id int not null);
 end

 insert tablea (id) values (1);
 insert tablea (id) values (2);
 insert tablea (id) values (3);


 insert tableb (id) values (1);
 --insert tableb (id) values (2);
 insert tableb (id) values (3);

  
  --Act
 -- nothing to do here
  
  --Assert
  exec tSQLt.AssertEqualsTable 'TableA', 'TableB'

END
Uncomment the commented out insert to make the test pass.
The failure message attempts to point you to what failed showing which rows matched and which failed to match.
tSQLt.AssertObjectEquals

For this test we once again need some setup (i.e. we need to create an object to test if it exists)

SELECT OBJECT_ID (N'dbo.fn_SUM', N'F')
IF OBJECT_ID (N'dbo.fn_SUM', N'F') IS NOT NULL
 DROP FUNCTION dbo.fn_sum
 
GO 
 
CREATE FUNCTION fn_SUM
(
 @a int,
 @b int
)
RETURNS int
BEGIN
 RETURN @a+@b;
END
And now the test
CREATE PROCEDURE [UnitTest_FirstGo].[test AssertObjectExists]
AS
BEGIN

  --Assert
  EXEC tSQLt.AssertObjectExists 'fn_SUM'

END

tSQLt.AssertResultSetsHaveSameMetaData

CREATE PROCEDURE [UnitTest_FirstGo].[test AssertObjectExists]
AS
BEGIN

  --Assemble
 if exists(select * 
 from information_schema.tables
 where table_schema='dbo' and table_name='tablea')
 begin
  drop table dbo.TableA;
  create table dbo.TableA (id int not null);
 end

 if exists(select * 
 from information_schema.tables
 where table_schema='dbo' and table_name='tablec')
 begin
  drop table dbo.TableC;
  create table dbo.TableC (id int not null, name varchar(50) not null);
 end

 insert tablea (id) values (1);
 insert tablea (id) values (2);
 insert tablea (id) values (3);


 insert tablec (id, name) values (5, 'andrew')
 
  --Act
 -- nothing to do here
  
  --Assert
  exec tSQLt.AssertResultSetsHaveSameMetaData 
   'select * from tablea',
   'select * from tablec'

END

That's it for now. I hope to get some time to look at how to isolate dependencies in the near future.

Wednesday, 4 April 2012

MSDTC Troubleshooting

Yesterday afternoon I was caught out by this error for a few hours.
A severe error occurred on the current command. The results, if any, should be discarded. OLE DB provider "SQLNCLI10" for linked server "LinkedServerName" returned message "No transaction is active.".
I had release the appliaction I am currently working on to testing.  The applcation has been runnin g perfectly on my development machine, but no such luck on our testers machine.  A new feature for this release is the use of distributed transactions to complete some of the business functionality.  This obviously requires that MSDTC is setup and a linked server is created on the local SQL Server. 

The MSDTC and linked server were setup on the test machine as I had described in my test document.

It took me a fair bit of googling to find the solution.  Most of the obvious setup problems were covered in my test document.  The problem in the end turned out to be the firewall on the test machine, something I should have checked much sooner.

Links for future reference:
http://stackoverflow.com/questions/673806/msdtc-how-many-ports-are-needed
http://support.microsoft.com/kb/306843
http://www.lewisroberts.com/2009/08/16/msdtc-through-a-firewall-to-an-sql-cluster-with-rpc/

Comparing Byte Arrays with Linq

This morning I ran into a small issue. I needed to extract a series of images from a database table. The problem I had was there were duplicate images in the database table and I needed a unique set of images written to disk. To filter the duplicates I have used the Enumerable.SequenceEqual linq operator. So here is how I did it.
using (var sqlConnection = new SqlConnection(connectionString))
{
 sqlConnection.Open();

 foreach (var deliveryNumber in deliveryNumbers)
 {
  
  var images = new List<byte[]>();

  // get all ewtphotos for this sapdeliverynumber 
  using (var sqlCommand = new SqlCommand())
  {
   sqlCommand.Connection = sqlConnection;
   sqlCommand.CommandType = CommandType.Text;
   sqlCommand.CommandText = "select imagedata " +
          "from delivery d " +
          "inner join image i on d.imageid=i.imageid " +
          "where d.deliverynumber='" + sapDeliveryNumber + "'";
   using (var sqlDataReader = sqlCommand.ExecuteReader())
   {
    while (sqlDataReader.Read())
    {
     var imageData = (byte[])sqlDataReader["ImageData"];

     var imageExists = images.Any(image => image.SequenceEqual(imageData));
     if (!imageExists) images.Add(imageData);

    }
   }

  }
  WriteImagesToDisk(sapDeliveryNumber, images);
 }

}

private static void WriteImagesToDisk(string deliveryNumber, List<byte[]> images)
{
 var counter = 1;
 foreach (var image in images)
 {
  var fileFullName = @"images\DeliveryNumber_" + deliveryNumber + "_" + counter + ".jpg";
  File.WriteAllBytes(fileFullName, image);
  counter++;
 }
}