THE SQL Server Blog Spot on the Web
Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | Join | Help
in Search

The Bit Bucket (Greg Low)

Ramblings of Greg Low

Data Driven Subscriptions in SQL Server 2005 Standard Edition

Recently, I was working at a client's site where SQL Server 2000 was still in use. They wanted to upgrade to SQL Server 2005. The only reason they were looking at the Enterprise Edition was for data-driven subscriptions in Reporting Services. The CEO certainly wasn't interested in paying the sort of dollars required for that, just to be able to automatically email out a set of monthly reports to about 30 or so folk within the company.

A quick bit of research led me to Jason L. Selburg's article at Code Project on how he achieved this by modifying the subscription details in the ReportServer database. You'll find his original post here: http://www.codeproject.com/KB/database/DataDrivenSubscriptions.aspx

I wanted to tackle a more complete version than Jason provided. There were a couple of key limitations:

1. You'd have to modify the stored procedure every time your parameter list changed. (and potentially have different stored procedures for different reports).

2. He used commands for manipulating the ntext columns that were deprecated. I wanted to avoid these.

3. The code didn't deal with concurrent usage.

Below, you'll find an alternate version of the proc. The concept is still the same. You create a subscription to a report, using tokens for parameters, set the subscription to happen in the past and then execute it using this proc in a scheduled Agent job. The different approach I've taken is:

1. I've provided the ability to deal with a parameter list rather than a single parameter.

2. I found that the ntext columns actually only contained simple xml. While I could have used XQuery to modify it, it's easier to directly manipulate it as varchar data (could have used nvarchar), given all we're doing is replacement of tokens with values. Either way, this avoids the use of UPDATETEXT etc.

3. I've used more of my style of naming conventions..

4. I've used a global temporary table as a flag to limit concurrent use. (Without this, the scheme could fail).

Hope you find it useful.

/*

    ExecuteSubscribedReport

 

      PROCEDURE DESCRIPTION:

      Creates the effect of a data driven subscription by replacing the fields in

      an existing subscription with the supplied values, executing the report

      and then replacing the original values.

 

      INPUT:

        @ScheduleID     The Job Name in SQL Server

        @EmailTo      The TO address of the email

        @EmailCC      The Carbon Copy address of the email

        @EmailBCC        The Blind Copy address of the email

        @EmailReplyTo  The Reply TO address of the email

        @EmailBody       Any text that you want in the email body

        @ParameterList The parameters for the report in the format 'Parameter1Token,Parameter1Value,Parameter2Token,Parameter2Value...'

                     Example: '|StartDate|,20071231,|Salesperson|,GE,|Region|,NW'

      OUTPUT:

        None

 

      WRITTEN BY:

      Greg Low based on a concept from Jason L. Selburg at CodeProject.com

 

    LIMITATIONS:

      ParameterTokens and ParameterValues are limited to 1000 characters

      EmailBody is limited to 8000 characters

      ParameterList is limited to 8000 characters total

*/

 

CREATE PROCEDURE dbo.ExecuteSubscribedReport

( @ScheduleID uniqueidentifier,

  @EmailTo varchar (1000) = NULL,

  @EmailCC varchar (1000) = NULL,

  @EmailBCC varchar (1000) = NULL,

  @EmailReplyTo varchar (1000) = NULL,

  @EmailBody varchar (8000) = NULL,

  @ParameterList varchar (8000) = NULL

)

AS BEGIN

 

  DECLARE @extensionSettingsPointer binary(16),

          @parametersPointer binary(16),

          @tokenPosition int,

          @tokenLength int,

          @subscriptionID uniqueidentifier,

          @parameterToken varchar(1000),

          @parameterValue varchar(1000),

          @parameterPosition int,

          @numberOfParameters int,

          @parameterCounter int,

          @character varchar(1),

          @parseStatus varchar(1), -- 0 ready for another token, 1 in a token, 2 in a value

          @originalExtensionSettings varchar(8000),

          @originalParameters varchar(8000),

          @newExtensionSettings varchar(8000),

          @newParameters varchar(8000);

  DECLARE @parameters TABLE (ParameterID int IDENTITY(1,1),

                             ParameterToken varchar(1000),

                             ParameterValue varchar(1000));

 

  -- first we need to unpack the parameter list

  IF @ParameterList IS NOT NULL BEGIN

    SET @parameterPosition = 1;

    SET @parseStatus = 0;

    SET @parameterToken = '';

    SET @parameterValue = '';

    SET @numberOfParameters = 0;

    WHILE @parameterPosition <= LEN(@ParameterList) BEGIN

      SET @character = SUBSTRING(@ParameterList,@parameterPosition,1);

      IF @character = ',' BEGIN

        IF @parseStatus = 0 BEGIN -- we had two commas in a row or the first character was a comma

          PRINT 'ParameterList has incorrect format';

          RETURN 1;

        END

        ELSE IF @parseStatus = 1 BEGIN -- we are at the end of the token

          SET @parseStatus = 2;

          SET @parameterValue = '';

        END

        ELSE BEGIN -- we are at the end of a value

          INSERT @parameters (ParameterToken,ParameterValue)

            VALUES (@ParameterToken,@ParameterValue);

          SET @numberOfParameters = @numberOfParameters + 1;

          SET @parseStatus = 0;

          SET @parameterToken = '';

        END;        

      END ELSE BEGIN

        IF @parseStatus = 0 BEGIN -- we have the first character of a token

          SET @parseStatus = 1;

          SET @parameterToken = @parameterToken + @character;

        END

        ELSE IF @parseStatus = 1 BEGIN -- we have another character in a token

          SET @parameterToken = @parameterToken + @character;

        END

        ELSE BEGIN -- we have another character in a value

          SET @parameterValue = @parameterValue + @character;

        END;

      END;

      SET @parameterPosition = @parameterPosition + 1;

    END;

    IF @parseStatus = 2 BEGIN-- we were still collecting a value

      INSERT @parameters (ParameterToken,ParameterValue)

        VALUES (@ParameterToken,@ParameterValue);

      SET @numberOfParameters = @numberOfParameters + 1;

    END;

  END;

 

  -- we need to wait for our turn at using the subscription system

  WHILE EXISTS(SELECT 1 FROM tempdb.sys.objects WHERE name = '##ReportInUse')

    WAITFOR DELAY '00:00:30';

  CREATE TABLE ##ReportInUse (ReportID int);

 

  -- once we have the parameters unpacked, we now need to find the subscriptionID

  SELECT @subscriptionID = SubscriptionID

    FROM dbo.ReportSchedule

    WHERE ScheduleID = @ScheduleID;

 

  -- next we save away the original values of ExtensionSettings and Parameters

  -- (we use them to make it easy put the values back later)

  -- they are actually xml but it'll be easier to work with them as strings

 

  SELECT @originalExtensionSettings = CAST(ExtensionSettings AS varchar(8000)),

         @originalParameters = CAST(Parameters AS varchar(8000))

    FROM dbo.Subscriptions

    WHERE SubscriptionID = @subscriptionID;

 

  SET @newExtensionSettings = @originalExtensionSettings;

  SET @newParameters = @originalParameters;

 

  -- if they have supplied arguments ie: not NULL and not blank, process them

  IF COALESCE(@EmailTo,'') <> ''

    SET @newExtensionSettings = REPLACE(@newExtensionSettings,'|TO|',@EmailTo);

  IF COALESCE(@EmailCC,'') <> ''

    SET @newExtensionSettings = REPLACE(@newExtensionSettings,'|CC|',@EmailCC);

  IF COALESCE(@EmailBCC,'') <> ''

    SET @newExtensionSettings = REPLACE(@newExtensionSettings,'|BC|',@EmailBC);

  IF COALESCE(@EmailReplyTo,'') <> ''

    SET @newExtensionSettings = REPLACE(@newExtensionSettings,'|RT|',@EmailReplyTo);

  IF COALESCE(@EmailBody,'') <> ''

    SET @newExtensionSettings = REPLACE(@newExtensionSettings,'|BD|',@EmailBody);

 

  IF @numberOfParameters > 0 BEGIN

    -- process each parameter in turn

    SET @parameterCounter = 1;

    WHILE @parameterCounter <= @numberOfParameters BEGIN

      SELECT @parameterToken = ParameterToken,

             @parameterValue = ParameterValue,

             @tokenLength = LEN(ParameterToken)

        FROM @parameters

        WHERE ParameterID = @parameterCounter;

      SET @newParameters = REPLACE(@newParameters,@ParameterToken,@ParameterValue);

      SET @parameterCounter = @parameterCounter + 1;

    END;

  END;

 

  -- Temporarily update the values

  UPDATE dbo.Subscriptions

    SET ExtensionSettings = CAST(@newExtensionSettings AS ntext),

        Parameters = CAST(@newParameters AS ntext)

  WHERE SubscriptionID = @subscriptionID;

 

  -- run the job

  EXEC msdb..sp_start_job @job_name = @ScheduleID

 

  -- make enough delay for the report to have started

  WAITFOR DELAY '00:00:30'

 

  -- put the original extensionsettings and parameter values back

  UPDATE dbo.Subscriptions

    SET ExtensionSettings = CAST(@originalExtensionSettings AS ntext),

        Parameters = CAST(@originalParameters AS ntext)

  WHERE SubscriptionID = @subscriptionID;

  -- finally we free up the subscription system for another person to use

  DROP TABLE ##ReportInUse;

END;

GO

EXEC dbo.ExecuteSubscribedReport

   @ScheduleID = '4CE38C83-6A03-4780-895A-92FD6F8FD5B0',

   @EmailTo = 'glow@solidq.com',

   @EmailCC = 'ozinfo@solidq.com',

   @EmailBCC = 'info@solidq.com',

   @EmailReplyTo = 'glow@solidq.com',

   @EmailBody = 'Hello Greg',

   @ParameterList = '|StartDate|,20071231,|Salesperson|,GE,|Region|,NW';

 

 

 


Published Wednesday, August 13, 2008 7:28 PM by Greg Low

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Stephen Morris said:

wouldn't an application Lock be easier than the global temp table ?

August 13, 2008 8:56 AM
 

jerryhung said:

Nicely done, bookmarked for future usage

August 13, 2008 10:15 AM
 

Greg Low said:

Hi Stephen,

Yep, they would have been another option. At first I was targeting SQL Server 2000 as well though.

Regards,

Greg

August 13, 2008 6:52 PM
 

Wile1one said:

Great bit of code Greg...

I wonder could you wrap this into a class object for recall by external procs...  rather than just a stored proc? Eg a remote data service could leverage this...

Cheers.

August 16, 2008 11:24 AM
 

loveSQL said:

Is there a bug to replace the new parameters?  I don't think the Parameter Token starts and ends with '|'.  

Why replace the token with value?i.e. SET @newParameters = REPLACE @newParameters,@ParameterToken,@ParameterValue);  

Should it not be replacing the old paramenter values with the new values?  I am lost.

August 19, 2008 8:16 PM
 

Greg Low said:

I have a feeling you might not have read the article that this refers to. What it suggested doing is making your parameter values be a token that wouldn't occur in the subscription ie: he set |TO| as the value for the EmailTo parameter. Then all we're doing is replacing these values before running the report.

August 19, 2008 9:21 PM
 

loveSQL said:

My bad.  I finally got it working.  Now, it works like a charm.  If a parameter value like '|CC|' is not supplied, it would show '|CC|'.  I replace all parameter values with '', if not supplied.  Thanks, Greg!

August 20, 2008 6:59 PM
 

Jeremy Finch said:

Hello,

I believe one further improvement, which I have already posted on Jason's message, is the removal of the wait time. What if the report takes minutes to run? What if you want to run it from a trigger?

That was the issue for me, and making data entry wait, was not acceptable.

Here is the Pseudo code:

Originally this SP was written to

1- make all declarations

2- get data to replace (this was dependent on the text being |TO| or something similar- static

3- replace data with new data

4- reset data back to static values to use SP again

New method-

1-make all declarations

2-reset data (just using an update)

3-get data

4-replace data

// note* you could also use the UPDATETEXT method, instead of Update

Update Subscriptions

Set ExtensionSettings = ~OriginalValue~

Where (SubscriptionID = @subscriptionID)

The original value, is the value the field has when the job is first created, or after the original SP was run, and the values were reset.

This allowed me to be rid of the delay.

Any questions just let me know.

Regards,

Jeremy

September 13, 2008 1:11 PM
 

Greg Low said:

Hi Jeremy,

The delay wasn't to run the report. It was just to give the report job time to start. I was going to do it by looking at the job status info but didn't get around to it.

And UPDATETEXT shouldn't be used now; it's deprecated.

Regards,

Greg

September 13, 2008 9:56 PM
 

Laura said:

You have code here to force a wait until nothing else is using the subscription system. (see comment "we need to wait for our turn at using the subscription system").

What happens if 2 reports try to use the subscription system at the same time?  What can you do to fix whatever this does?

November 5, 2008 12:33 PM
 

Greg Low said:

Hi Laura,

That scenario is fine. That's what my comment #4 was about. I've used a global temp table to serialise access to it until it's launched.

Regards,

Greg

November 5, 2008 11:27 PM

Leave a Comment

(required) 
(optional)
(required) 
Submit