THE SQL Server Blog Spot on the Web

Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | |
in Search

The Bit Bucket (Greg Low): IDisposable

Ramblings of Greg Low (SQL Server MVP, MCM and Microsoft RD) - SQL Down Under

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
 

Paula said:

Greg,

 got any updates on this?  i'm stuck using this, and the waitime is either too long or too short.  at times my reports show the SU because they got set to fast, or the subject line NEVER gets set back to SU.  any suggetions?

thanks!

Paula

April 24, 2009 5:20 PM
 

Doug Rixmann said:

Maybe I missed something but because I didn't like leaving all my parameters as strings, I added this function that replaces the value of a certain parameter (ex. "DatabaseTableID").

Basically,

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

Would be replaced by:

SET @newParameters = dbo.ReplaceParameterValue(@newParameters,@ParameterToken,@ParameterValue);

CREATE FUNCTION [dbo].[ReplaceParameterValue]

(

-- Add the parameters for the function here

@ParameterString varchar(max),

@Parameter varchar(max),

@Value varchar(max)

)

RETURNS varchar(max)

AS

BEGIN

DECLARE

@NPOS int,

@NPOSSTARTVALUE int,

@NPOSENDVALUE int,

@NLENGTH int

SELECT @NPOS = CHARINDEX('<Name>' + @Parameter, @ParameterString, 0) + 6

SELECT @NPOSSTARTVALUE = CHARINDEX('<Value>', RIGHT(@ParameterString, LEN(@ParameterString) - @NPOS + 1), 0)

SELECT @NPOSENDVALUE = CHARINDEX('</Value>', RIGHT(@ParameterString, LEN(@ParameterString) - @NPOS + 1), 0)

SELECT @NLENGTH = @NPOSENDVALUE - @NPOSSTARTVALUE - 6 - 1

RETURN LEFT(@ParameterString, @NPOS + @NPOSSTARTVALUE + 5)

+ @Value

+ RIGHT(@ParameterString, LEN(@ParameterString) - (@NPOS + @NPOSSTARTVALUE + @NLENGTH) - 5)

END

November 2, 2009 3:54 PM
 

Oliver Holloway said:

Greg, is the table usage different in SSRS 2008? Seems like the proc expects the first placeholders to be in the extensionsettings field, but they're all in the parameters field when I create the schedule.

Paula, there are various ways to resolve the waittime. One is to check for the most recent finished runtime, described in Jason's article's forum, in the last post at the bottom of page 1:  http://www.sqlservercentral.com/Forums/Topic283642-304-1.aspx

November 2, 2009 4:26 PM
 

Guillaume said:

Hi,

Does your code work with SQL Server 2008 ?

Thanks,

Guillaume

December 15, 2009 9:06 AM
 

Guillaume said:

Yes, that's working :)

Thanks

January 6, 2010 5:42 AM
 

Darko said:

Thank you Greg! And thank you Doug, too. Your solution made an excellent solution even better!

Few notes:

For Parameter Tokens (e.g. "Comment") to actually be present in the ExtensionSettings XML, one has to add some dummy values first into the appropriate fields in the RS Subscription Scheduler.

Speaking of the Comment field (in Reporting Services subscription), it can be fully HTML formatted, as long as angle brackets (and other symbols capable of confusing the XML parser) are properly substituted with &lt;, &gt; etc.

In my solution, I am passing additional arguments (such as Comment, Subject, Priority, RenderFormat, etc), passing them like this:

SET @newExtensionSettings = dbo.ReplaceParameterValue(@newExtensionSettings,'TO',@EmailTo);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'CC',@EmailCC);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'BCC',@EmailBCC);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'ReplyTo',@EmailReplyTo);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'Comment',@EmailBody);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'RenderFormat',@RenderFormat);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'Priority',@Priority);

SET @newExtensionSettings =  dbo.ReplaceParameterValue(@newExtensionSettings,'Subject',@Subject);

(...)

March 23, 2010 4:07 PM
 

Erik Scofield said:

Good job with the changes.

Thank you Jason L. Selburg and Greg Low

I have made some changes myself.  

First, I was not happy with the shortcomings of "Replace"

So I converted

@newParameters varchar(8000)  To @newParameters xml

And changed the parameter looping.

  WHILE @parameterCounter <= @numberOfParameters BEGIN

     SELECT @parameterToken = ParameterToken,

            @parameterValue = ParameterValue,

            @tokenLength = LEN(ParameterToken)

       FROM @parameters

       WHERE ParameterID = @parameterCounter;

SET @newParameters.modify('replace value of (/ParameterValues/ParameterValue/Name[text()=sql:variable("@ParameterToken")]/../Value/text())[1] with sql:variable("@ParameterValue")')        

SET @parameterCounter = @parameterCounter + 1;

   END;

Also, the locking just did not work for me, well the locking does, but we need a way to tell with the job has completed and I also changed from executing the job to triggering the event myself.

declare @repID uniqueidentifier

select @repID = reportid from ReportSchedule where SubscriptionID = @subscriptionID

declare @lastRunID bigint, @newRunID bigint

select top 1 @lastRunID = logentryid from ExecutionLogStorage where ReportID = @repID order by LogEntryId desc

print @lastRunID

 -- run the job

 --EXEC msdb..sp_start_job @job_name = @ScheduleID

exec ReportServer.dbo.AddEvent @EventType='TimedSubscription', @EventData=@subscriptionID

--  WAITFOR DELAY '00:00:03';

 -- make enough delay for the report to have started

 WHILE NOT EXISTS(SELECT top 1 logEntryID FROM ExecutionLogStorage where logEntryID > @lastRunID and ReportID = @repID order by LogEntryId desc)

   WAITFOR DELAY '00:00:03';

Do the proc is complete.  It will exit once and finally the job is completely executed with in a 3 second window.

Cheers

May 13, 2010 8:55 PM
 

Jimmy George said:

Hi,

I am on an SQL2008 ENT SSRS deployment. More than 250 reports are to be delivered daily via fileshare and e-mail using data driven subscriptions. The reports are run based on multiple report parameter values ( say customer=subscription, fromdatetime and todatetime). The recurring reports need to check the last run status. If failed, it has to be run for the period covering the failed interval else the normal interval.

Can multiple report parameter values ( Customer, starttime, endtime) be derived using a TSQL stored proc and adjust the parameters and passed on to subscriptions at run time directly from the ReportServer databases ?

Cheers !

Jimmy

May 18, 2010 2:14 AM
 

Silvia said:

Thanks very much, Greg and Jason.

It works perfectly!!!!!!!!!

A great help.

November 29, 2010 2:39 PM
 

Isabel said:

Hi Greg,

I would like to pass multivalue parameters.

Is this possible?

Thx in advance.

January 27, 2011 10:25 AM
 

Victor said:

Does anyone have a solution for sending the report to a file share as well ?

February 14, 2011 2:20 PM
 

Kanaka said:

Thanks a lot.

February 2, 2012 4:47 PM
 

Sanjay said:

For some reason the parameter is not picked from the variable when executing the sp. Can anyone help?

April 18, 2012 11:58 PM
 

MGisonda said:

Anyone having a problem where the slows down. I am utilizing this to send out invoice. After about 50 invoices, the job slows down to a crawl. To pump out 140 invoices, it can take up to 9 hours to run. What's funny is that when running it manually (not through a job) it does the same thing. Visually though, it appears to get hung up after a few, then it suddenly bursts through with a few.

Anyone else having something like this happening? Any ideas where to look?

Mike

mgisonda@leespring.com

April 25, 2013 9:12 AM
 

Greg Low said:

Subscriptions can write out to file shares. No difference with this type of subscription.

April 25, 2013 9:18 PM
 

Tony Z said:

Thanks for this great work.

I have one problem though....

If my parameter is a list for use in a WHERE FLD1 IN ('1','2','3')

my parameter string wound look like

|fld1|,1,2,3,|next_param....

This isn't handled and I don't think it can be.  The only solution I can think of is to load a pvalue table and use it in the report...

WHERE FLD1 IN (select fld1vals from pvalues) and don't pass a parameter to the report.

Is that about it?

thanks

tonyzoc@gmail.com

May 19, 2013 4:00 PM
 

Greg Low said:

Hi Tony,

That would work. An alternative would be to use XML for the parameters, and then split them out of there.

Regards,

Greg

May 19, 2013 6:18 PM

Leave a Comment

(required) 
(required) 
Submit
Powered by Community Server (Commercial Edition), by Telligent Systems
  Privacy Statement