Driving your business rules dynamically via T-SQL | Towards Data Science

Hardcoding. I’m sure at this point you’ve heard all about how using it will take you to coder’s hell. Experts seem to recommend avoiding it whenever possible. So why are most of our queries hardcoded? What if we could dictate the query’s predicate in a centralized, interchangeable fashion? Let us find out just how dynamic (T-)SQL is.
Note, however, that this might not be a good idea, but let's have some fun anyway.
In order to illustrate the possibilities data-driven constraints introduce, we’re going to create a data validator which enforces a set of constraints (not to be confused with SQL’s own constraints), business rules or simply thresholds, which are defined on a table. In addition, the same model can be extended to define your where predicate, or any part of your query. If anyone does find this post relevant — doubt — I will create another post with the extension.
Our end goal is then to obtain a Stored Procedure which allows one to validate a given entry based on a given set of dynamic rules. For instance, validate if the value 50 abides by RuleSet ID 2, and retrieve the relevant output code:
exec ValidateThreshold 2, ‘50’, @returned_code OUTPUT;
First things first, why on earth would you want to do this? Who hardcodes business rules into queries on the DB anyway? Well, it turns out, most of us. Ranging from data model proximity to responsibility decoupling, there are plenty of reasons why such could be required.
Consider an example of an auction website. Today you have a rule saying you will only accept bids no smaller than 0.05. What if tomorrow you want to change said value to anything between 0.01 and 10% of the product’s appraisal? Will you change every procedure, view, trigger or query containing the new constraint? Well hopefully after this post, you won't have to anymore.
In reality, we are introducing a simple model, in practice, it would make sense to have a RuleSets table, which creates groups of rules to be applied.
An introductory note is required, I am by no means an SQL expert — as I’m sure you noticed — on the contrary, I’m just an extremely lazy person who likes to find ways to prevent manual labour. Hopefully, it will help your lazy persona as well. Make sure to read the ending note, it might be the most important part of this whole post.
The first step would be to actually structure your rules or thresholds.
Create table Threshold( ThresholdID int identity not null, ThresholdSetID int not null, VarName varchar(20) not null, Operator varchar(2) not null, TargetValue varchar(50) not null, [Description] varchar(100) not null, ThresholdOrder int not null, Deprecated bit not null default 0,primary key (ThresholdID));
And populate it with a set of dummy content:
insert into Threshold values (1, '@input', '>=', '25', 'dbo.InsertAuctionBid(...)', 1, 0);insert into Threshold values (1, '@input', '<', '(select max(value) from dbo.AuctionDates)', 'dbo.InsertAuctionBid(...)', 2, 0);
Note how the rows@input argument, which represents the value to be validated. The first rule will ensure our argument is larger or equal to 25, whereas the second rule ensures the passed argument is below the maximum date found in the AuctionDates table.
Onto the validation Procedure!
create procedure dbo.ValidateThreshold( @thresholdID int, @input nvarchar(50), @return_code int OUTPUT)as if @thresholdID is null  begin  set @return_code = 2;  return; endif @input is null   begin  set @return_code = 2;  return; enddeclare @statement nvarchar(max), @where_clause nvarchar(max);set @where_clause = stuff((  select '  and '+ VarName +' '+ Operator  +' ' + TargetValue +char(10)  from    Threshold  where   ThresholdID in (@thresholdID)  order by    thresholdOrder  for xml path (''), type).value('.','nvarchar(max)')  ,1,6,'');set @statement = 'if exists(select 1 '+char(10)+'where '+ @where_clause + char(10) + ') select 0; else select 1;';set @statement = replace(@statement, '@input', @input)exec sp_executesql  @query = @statement  ,@params = N'@return_code INT OUTPUT'  ,@return_code = @return_code OUTPUT;
There’s a lot to take in for those of us who are fairly new to SQL, so lets split this into chunks.
Our procedure should have three arguments: 2 inputs and 1 output. The rule to be applied; the parameter to be validated, formatted as a text; and finally the operation’s output code, 0 being successful, anything different representing a specific error code.
create procedure dbo.ValidateThreshold(  @thresholdGroup int,  @input nvarchar(50),  @return_code int OUTPUT)
Next, validate our inputs and return an appropriate error code
as if @thresholdID is null  begin  set @return_code = 2;  return; endif @input is null   begin  set @return_code = 2;  return; end
So far so good. Here comes the juice
declare @statement nvarchar(max), @where_clause nvarchar(max);-- Generate the underlying where clause by concatenatinc the Variable to test, the boolean operator and the test predicate -- using XML PATH to repeat the whole process, gluing subsequent predicates with an ' and'. -- Lastly, use stuff to collapse it all together, removing the first 6 characters (corresponding to ' and') starting on the first position. -- Note: If more than a @thresholdID is provided, the predicates will be passed according to Threshold.ThresholdOrder set @where_clause = stuff((  select '  and '+ VarName +' '+ Operator  +' ' + TargetValue +char(10)  from    Threshold  where   ThresholdID in (@thresholdID)  order by    thresholdOrder  for xml path (''), type).value('.','nvarchar(max)')  ,1,6,'');set @statement = 'if exists(select 1 '+char(10)+'where '+@where_clause + char(10) + ') select 0; else select 1;';-- Feed the input variable set @statement = replace(@statement, '@input', @input)exec sp_executesql  @query = @statement  ,@params = N'@input nvarchar(50), @return_code INT OUTPUT'  ,@input = @value  ,@return_code = @return_code OUTPUT;
FOR XML PATH allows you to convert the query’s result into an xml file, as the name suggests. However, by passing an empty string into the RAW argument, we create a concatenated string for the returned data. Consider the following users table:
╔═══════════╗║   Name    ║╠═══════════╣║ John      ║║ Mary      ║║ William   ║║ Anthony   ║╚═══════════╩
Applying to the previous table:
select ','+ name         from Users        FOR XML PATH('')Output: ‘,John, Mary, William, Anthony’
Notice the trailing comma, which whilst allowing us to create a comma-separated string, would completely ruin an SQL statement. In our case, instead of the comma we entered an and keyword so we can glue multiple clauses together.
We start by calling the value() function to convert the XML object into a string. We then use the STUFF() function to retrieve the comma, or in our case, 6 characters to the remove the ‘ and ’ portion from our generated string.
The next step is to pass the generated where clause onto a boolean statement using Exists to test for the presence of a return. If the return is 1, then the @input is valid, if however the return is null, then the @input fails the validation.
The process ends by replacing the @input field with the passed @input and executing the query via Dynamic SQL.
Note however, that dynamic SQL has a lot of drawbacks, from performance to security. I am by no means suggesting your DBA should implement this model, it is merely a presentation of (T-)SQL’s flexibility where one does not expect it to exist.
I hope you found this resource helpful or at least, interesting.