1

I want to create a stored procedure in SQL Server 2017 and let it be called somewhere else (i.e., Python). It accepts three parameters, stkname - stock name, stktype - stock type, colname - output columns (only these columns are returned). @colname is a varchar storing all required column names separated by ,.

  • If @colname is not specified, return all columns (*)
  • If stkname or stktype is not specified, do not filter by it

This is my code so far:

create procedure demo 
     (@stkname varchar(max), 
      @colname varchar(max), 
      @stktype varchar(max)) 
as
begin
    ----These lines are pseudo code -----
    select 
        (if @colname is not null, @colname, else *) 
    from 
        Datatable 
    (if @stkname is not null, where stkname = @stkname)
    (if @stktype is not null, where stktype = @stktype)

    ---- pseudo code end here-----
end

The desired result is that

exec demo @colname= 'ticker,price', @stktype = 'A'

returns two columns - ticker and price, for all records with stktype = 'A'

I could imagine 'dynamic SQL' would be possible, but not that 'elegant' and I need to write 2*2*2 = 8 cases.

How can I actually implement it in a better way?

PS: This problem is not a duplicate, since not only I need to pass column names, but also 'generate a query by other parameters'

MTANG
  • 508
  • 5
  • 16
  • 1
    You will need dynamic SQL. You want an "elegant" solution for a very non-elegant problem. "Just select whatever columns you like, and add any where causes you like" is not exactly elegant. – Aaron Bertrand Oct 31 '18 at 19:31
  • @Sami Any possibilities I still use dynamic SQL but not write 8 separate cases? – MTANG Oct 31 '18 at 19:32
  • @AaronBertrand Yes. But it's hard to prevent malicious behavior if I use dynamic SQL – MTANG Oct 31 '18 at 19:34
  • 1
    It's not, actually. – Aaron Bertrand Oct 31 '18 at 19:42
  • Possible duplicate of [Can I pass column name as input parameter in SQL stored Procedure](https://stackoverflow.com/questions/10092869/can-i-pass-column-name-as-input-parameter-in-sql-stored-procedure) – Elaskanator Oct 31 '18 at 19:50
  • @Elaskanator It's not. Since I also need to generate different query based on whether other arguments are null – MTANG Oct 31 '18 at 19:56
  • 1
    This has design flaws in capital letters all over the place. – Sean Lange Oct 31 '18 at 20:05
  • The other parameters are simple filters which are *much* easier to handle than dynamic column selection (e.g. `... WHERE (@Param IS NULL OR [CorrespondingColumn] = @Param) AND...`) which does not change the query code itself. If you had instead requested the filtering parameters to be also against dynamically-specified columns, then it definitely would be a different question. – Elaskanator Oct 31 '18 at 20:26
  • @SeanLange Picking at code style is [straw-man arguing](https://en.wikipedia.org/wiki/Straw_man) if it has no performance or functional impact. Although I agree that code should be held to high standards ;) – Elaskanator Oct 31 '18 at 20:28
  • @Elaskanator same could be said for your comment. And on the contrary, this type of dynamically building queries has both a functional and performance impact. When we are dealing with queries if you have to jump through really complicated code it is usually a sign that the design is flawed. – Sean Lange Oct 31 '18 at 20:30
  • @SeanLange I don't understand what you mean that the "same could be said for [my] comment." I do not see any considerable difference between this SO question and the one I linked to. – Elaskanator Oct 31 '18 at 20:35
  • @Elaskanator The main purpose for any active users should be answering questions, not trying to 'mark other's question as duplicates'. And the link you referred DOES NOT solve my problem, but Aaron's answer does. – MTANG Oct 31 '18 at 20:39
  • 1
    I was talking about you claiming I was making a straw man argument. But your claim could also be straw man argument. In the case of this question it needed to be said that the real issue is the design, not the solution they are trying to achieve. Also commonly known as an [xy problem](http://xyproblem.info/). The link you posted is absolutely relevant and I agree that it is basically the same question. :) – Sean Lange Oct 31 '18 at 20:39
  • I suppose the other question doesn't have the greatest answers, and a decent bit of work is needed on the [best-fitting one](https://stackoverflow.com/a/10092933/2799848) I'm seeing to handle multiple columns and explain how to use the parameterization of `sp_executesql`. Perhaps I was too hasty, but your question really feels like a well-known thing to the community, but more full-fledged as you also actually want to validate the request. Even [this answer](https://stackoverflow.com/a/8176545/2799848) has a bit of a ways to go for your request. – Elaskanator Oct 31 '18 at 20:52

1 Answers1

4

You need dynamic SQL, but you don't need to write a case for every permutation, and it's really not all that hard to prevent malicious behavior.

CREATE PROCEDURE dbo.demo -- always use schema prefix
  @stkname  varchar(max)  = NULL,  
  @colnames nvarchar(max) = NULL, -- always use Unicode + proper name
  @stktype  varchar(max)  = NULL
AS
BEGIN
   DECLARE @sql nvarchar(max);

   SELECT @sql = N'SELECT ' 
       + STRING_AGG(QUOTENAME(LTRIM(RTRIM(x.value))), ',') 
     FROM STRING_SPLIT(@colnames, ',') AS x
     WHERE EXISTS 
     (
        SELECT 1 FROM sys.columns AS c 
        WHERE LTRIM(RTRIM(x.value)) = c.name
        AND c.[object_id] = OBJECT_ID(N'dbo.DataTable')
     ); 

   SET @sql += N' FROM dbo.DataTable WHERE 1 = 1'
     + CASE WHEN @stkname IS NOT NULL THEN 
            N' AND stkname = @stkname' ELSE N'' END
     + CASE WHEN @stktype IS NOT NULL THEN 
            N' AND stktype = @stktype' ELSE N'' END;

  EXEC sys.sp_executesql @sql,
     N'@stkname varchar(max), @stktype varchar(max)',
     @stkname, @stktype;
END

It's not clear if the stkname and stktype columns really are varchar(max), I kind of doubt it, you should replace those declarations with the actual data types that match the column definitions. In any case, with this approach the user can't pass nonsense into the column list unless they somehow had the ability to add a column to this table that matched their nonsense pattern (and why are they typing this anyway, instead of picking the columns from a defined list?). And any data they pass as a string to the other two parameters can't possibly be executed here. The thing you are afraid of is probably a result of sloppy code where you carelessly append user input and execute it unchecked, like this:

SET @sql = N'SELECT ' + @weapon_from_user + '...';

For more on malicious behavior see:

Aaron Bertrand
  • 272,866
  • 37
  • 466
  • 490
  • Thanks! I forgot the 'where 1 = 1' trick to prevent writing cases for using 'where' or 'and'. However the @colname in my case is not a single name but might be many column names separated by ',' (Or I can change the input formatting for different separators) – MTANG Oct 31 '18 at 19:44
  • 2
    @MTANG that's a terrible parameter name then. – Aaron Bertrand Oct 31 '18 at 19:46
  • Yes, maybe 'colnames' would be more meaningful here @Aaron Bertrand – MTANG Oct 31 '18 at 19:47
  • Just [split the parameter value](https://stackoverflow.com/questions/9811161) and do a left exclusive join on `sys.columns` to check for illegal values in the parameter string. I have had to implement such a validation for [dynamic sorting queries](https://stackoverflow.com/questions/15085149) (without hardcoding a big nasty `CASE` statement for each use). – Elaskanator Oct 31 '18 at 19:49
  • @AaronBertrand This works perfectly! Just for curiosity, why stkname and stktype are added after EXEC sys.sp_executesql (but not the colname) – MTANG Oct 31 '18 at 20:09
  • Because the two where clauses are direct, strongly-typed parameter value evaluations. `@colnames` isn't used in the dynamic SQL, it was used to build the SQL. It's no longer needed. – Aaron Bertrand Oct 31 '18 at 20:12
  • What do you mean by 'strongly-typed parameter value evaluations'. Is that because these variables appear in 'where' clause that a 'type clarification' is needed? Or because we write 'THEN N' AND stkname = @stkname'' – MTANG Oct 31 '18 at 20:28
  • The second argument of [`sp_executesql`](https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-executesql-transact-sql) specifies the scalar variables that it can use, to avoid string concatenation to use those values in the dynamic SQL (the first argument) which is [a big security risk](https://www.aspsnippets.com/Articles/SQL-Server-spexecutesql-example-Passing-parameters-to-spexecutesql-in-SQL-Server.aspx). It also helps with writing cleaner code when you have to use dynamic SQL. – Elaskanator Oct 31 '18 at 20:42
  • @Elaskanator Thanks! – MTANG Oct 31 '18 at 21:28