Nielsen c28.tex V4 - 07/23/2009 4:58pm Page 672 Part IV Developing with SQL Server Summary The data abstraction layer is a key component of your database architecture plan and it plays a major role in determining the future extensibility and maintenance costs of the database. Even when it seems that the cost of developing a data abstraction layer and refactoring the existing application code to hit the data abstraction layer instead of tables might be prohibitively expensive, savvy IT or product managers understand that in the long run it will save money — and their job. 672 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 673 Dynamic SQL and Code Generation IN THIS CHAPTER Executing dynamic SQL Parameterized queries The risk of SQL injection Generating stored procedures Alternatives to dynamic SQL F olks laugh when they hear that my favorite project is based on the notion that T-SQL is a great language for code generation. Nordic (New Object/Relational Design) is essentially a code-generation tool that uses dynamic SQL to create tables, stored procedures, and views. T-SQL works rather well for code generation, thank you. The term dynamic SQL has a couple of differing definitions. Some say it describes anySQLquerysubmittedbyaclientother than a stored procedure. That’s not true. SQL submitted from the client is better known as ad-hoc SQL. It’s more accurate to say that dynamic SQL describes any SQL DML statement assembled dynamically at runtime as a string and then submitted. Dynamic SQL is very useful for several tasks: ■ Multiple possible query criteria can be dynamically assembled into custom FROM, WHERE,andORDER BY clauses for flexible queries. ■ Code can respond to the schema of the database and generate appropri- ate triggers, CRUD stored procedures, and views. ■ Dynamic code can auto-generate very consistent stored procedures. However, note the following issues when developing dynamic SQL: ■ Dynamic SQL that includes user entries in WHERE clauses can be open to SQL injection attacks. ■ Poorly written dynamic SQL queries often include extra table references and perform poorly. ■ T-SQL code that generates T-SQL code can be tricky to debug. 673 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 674 Part IV Developing with SQL Server Executing Dynamic SQL The EXECUTE command, or EXEC for short, creates a new instance of the batch as if it were a batch submitted from some client to the server. While the EXECUTE command is normally used to call a stored procedure, it can also be used to execute a T-SQL query or batch: EXEC[UTE] (’T-SQL batch’); For example, the following EXEC command executes a simple SELECT statement: USE Family; EXEC (’SELECT LastName FROM Person WHERE PersonID = 12;’); Result: LastName Halloway The security context of executing code should be considered when working with the EXECUTE command. You can control which user account the Database Engine uses to validate permissions on objects that are referenced by the module. The following code uses the EXECUTE AS syntax to execute the query as the user Joe: Use OBXKites EXECUTE AS ‘Joe’ select * from Products Another aspect of the EXECUTE command is the capability to execute the code at a linked server, instead of at the local server. The code is submitted to the linked server and the results are returned to the local server: EXECUTE (’Code’) AT MAUI/SYDNEY; sp_executeSQL Another method of executing dynamic SQL is to use the sp_executesql system stored procedure. It offers greater compatibility with complex SQL queries than the straight EXECUTE command. In several situations I have found that the EXECUTE command failed to execute the dynamic SQL, but sp_executesql worked flawlessly: EXEC sp_Executesql ‘T-SQL query’, ‘Parameters Definition’, Parameter, Parameter ; Parameterized queries Sometimes it’s easier to create queries based on a number of parameters because it usually avoids the need for concatenating strings. The query and the definition must be Unicode strings. Parameters provide optimization. If the T-SQL query has the same parameters for each execution, then these parameters can be passed to sp_executesql so the SQL query plan can be stored, and future executions will be optimized. The following example executes the same query from the Person table 674 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 675 Dynamic SQL and Code Generation 29 in the Family database, but this example uses parameters (the N before the parameters is necessary because sp_executesql requires Unicode strings): EXEC sp_executesql N’SELECT LastName FROM Person WHERE PersonID = @PersonSelect;’, N’@PersonSelect INT’, @PersonSelect = 12; Result: LastName Halloway Developing dynamic SQL code Building a dynamic SQL string usually entails combining a SELECT column’s literal string with a more fluid FROM clause and WHERE clause. While any part of the query can be dynamic, normally the SELECT @columns is not. Once the SQL string is complete, the SQL statement is executed by means of the sp_executesql command. The example that follows builds both custom FROM and WHERE clauses based on the user’s requirements. Within the batch, the NeedsAnd bit variable tracks the need for an And separator between WHERE clause conditions. If the product category is specified, then the initial portion of the SELECT statement includes the required joins to fetch the ProductCategory table. The WHERE clause portion of the batch examines each possible user criterion. If the user has specified a criterion for that column, then the column, with its criterion, is added to the @SQLWhere string. Real-world dynamic SQL sometimes includes dozens of complex options. The following code listing uses three possible columns for optional user criteria: USE OBXKites; DECLARE @SQL NVARCHAR(1024), @SQLWhere NVARCHAR(1024), @NeedsAnd BIT, User Parameters @ProductName VARCHAR(50), @ProductCode VARCHAR(10), @ProductCategory VARCHAR(50); Initialize Variables SET @NeedsAnd = 0; SET @SQLWhere = ‘’; 675 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 676 Part IV Developing with SQL Server Simulate User’s Requirements SET @ProductName = NULL; SET @ProductCode = 1001; SET @ProductCategory = NULL; Assembly Dynamic SQL Set up initial SQL Select IF @ProductCategory IS NULL SET @SQL = ‘Select ProductName from Product’; ELSE SET @SQL = ‘Select ProductName from Product Join ProductCategory on Product.ProductCategoryID = ProductCategory.ProductCategoryID’; Build the Dynamic Where Clause IF @ProductName IS NOT NULL BEGIN; SET @SQLWhere = ‘ProductName = ’ + @ProductName; SET @NeedsAnd = 1; END; IF @ProductCode IS NOT NULL BEGIN; IF @NeedsAnd = 1 SET @SQLWhere = @SQLWhere + ‘ and ’; SET @SQLWhere = ‘Code=’+@ProductCode; SET @NeedsAnd = 1; END; IF @ProductCategory IS NOT NULL BEGIN; IF @NeedsAnd = 1 SET @SQLWhere = @SQLWhere + ‘ and ’; SET @SQLWhere = ‘ProductCategory = ’ + @ProductCategory; SET @NeedsAnd = 1; END; Assemble the select and the where portions of the dynamic SQL IF @SQLWhere <> ‘’ SET @SQL = @SQL + ‘ where ’ + @SQLWhere + ‘;’; ∼∼Use this for testing and debug use only. PRINT @SQL; EXEC sp_executesql @SQL 676 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 677 Dynamic SQL and Code Generation 29 The results shown are both the printed text of the dynamic SQL and the data returned from the execu- tion of the dynamic SQL statement: Select ProductName from Product where Code = 1001; Name Basic Box Kite 21 inch Code generation The following example may seem a bit complex, but it’s a great real-world demonstration of T-SQL code generation. It’s from the Nordic database and this is the piece of code that actually generates the stored procedures and views for each class. This procedure is called every time a new class or attribute is added or changed. A few points in the code worth noting: ■ + CHAR(13) + CHAR(10) are added to the generated code to make i t more readable. ■ The columns (attributes) are built up in the @SQLStr variable first, then the dynamic FROM clause is added. ■ A cursor is used to iterate through the columns and tables to build up the custom stored procedure and view. ■ The @SQLStr is assembled once and the @GenStr is modified to first create the view and then create the stored procedure. ■ The dynamic SQL variables @SQLStr and @GenStr are both declared as NVARCHAR(MAX). ■ Any custom columns are automatically enclosed in square brackets to avoid any syntax errors. Here’s the code: Gen Class CREATE alter PROC dbo.GenClass (@ClassName NVARCHAR(50)) AS SET NoCount ON EXEC IncVersion ‘Class Design’ DECLARE @GenStr NVARCHAR(MAX), @SQLStr NVARCHAR(MAX), @ClassStr NVARCHAR(MAX), @CurrentClass CHAR(100), 677 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 678 Part IV Developing with SQL Server @CurrentAttrib CHAR(100) SET @ClassName = REPLACE(@ClassName, ‘ ’, ‘’) data from Object Table SET @SQLStr = ‘SELECT o.ObjectID, o.ClassID, c.ClassName, o.StateID, ’ + CHAR(13) + CHAR(10) + ‘ sc.ClassName + ’‘:’‘ + StateName as [State],’ + CHAR(13) + CHAR(10) + ‘ ObjectCode as [Object:ObjectCode], Name1 as [Object:Name1], Name2 as [Object:Name2],’ + CHAR(13) + CHAR(10) + ‘ NULL as [Object:Description],’ + CHAR(13) + CHAR(10) + ‘ o.Created as [Object:Created], o.Modified as [Object:Modified], o.Version as [Object:Version] ’ + CHAR(13) + CHAR(10) Walk through custom attributes DECLARE cAttributes CURSOR FAST_FORWARD FOR SELECT REPLACE(ClassName, ‘ ’, ‘’), AttributeName FROM dbo.Attributes(@ClassName) OPEN cAttributes prime the cursor FETCH cAttributes INTO @CurrentClass, @CurrentAttrib WHILE @@Fetch_Status = 0 BEGIN SET @SQLStr = @SQLStr + ‘ , CC’ + RTRIM(@CurrentClass) + ‘.’ + RTRIM(@CurrentAttrib)+‘as[’+RTRIM(@CurrentClass) + ‘:’ + RTRIM(@CurrentAttrib) + ‘]’ + CHAR(13) + CHAR(10) fetch next FETCH cAttributes INTO @CurrentClass, @CurrentAttrib END CLOSE cAttributes DEALLOCATE cAttributes FROM base metadata tables SET @SQLStr = @SQLStr + ‘ FROM dbo.Object AS o’ + CHAR(13) + CHAR(10) + ‘ JOIN dbo.Class AS c’ + CHAR(13) + CHAR(10) + ‘ ON o.ClassID = c.ClassID’ + CHAR(13) + CHAR(10) + ‘ LEFT JOIN dbo.State AS s’ + CHAR(13) + CHAR(10) + ‘ ON o.StateID = s.StateID’ + CHAR(13) + CHAR(10) + ‘ LEFT JOIN dbo.Class AS sc’ + CHAR(13) + CHAR(10) + ‘ ON s.ClassID = sc.ClassID’ + CHAR(13) + CHAR(10) FROM dynamic classes DECLARE cClasses CURSOR FAST_FORWARD Set Difference Query FOR SELECT REPLACE(ClassName, ‘ ’, ‘’) 678 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 679 Dynamic SQL and Code Generation 29 FROM SuperClasses(dbo.GetClassID(@ClassName)) ORDER BY ClassID DESC OPEN cClasses FETCH cClasses INTO @CurrentClass prime the cursor WHILE @@Fetch_Status = 0 BEGIN SET @SQLStr = @SQLStr + ‘ JOIN dbo.Obj’ + RTRIM(@CurrentClass)+‘CC’ + RTRIM(@CurrentClass) + CHAR(13) + CHAR(10) + ‘ ON o.ObjectID = CC’ + RTRIM(@CurrentClass) + ‘.ObjectID’ + CHAR(13) + CHAR(10) FETCH cClasses INTO @CurrentClass fetch next END CLOSE cClasses DEALLOCATE cClasses Drop and Create View SET @GenStr = ‘IF OBJECT_ID(’’v’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) + ‘’’)’+‘ISNOTNULL DROP VIEW dbo.v’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) EXEC sp_executesql @GenStr SET @GenStr = ‘CREATE VIEW dbo.v’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) + CHAR(13) + CHAR(10) + ‘ AS ’ + CHAR(13) + CHAR(10) + @SQLStr EXEC sp_executesql @GenStr Standard Where Clause SET @SQLStr = @SQLStr + CHAR(13) + CHAR(10) + ‘ WHERE o.ObjectID = @ObjectID weirdness aboundeth’ Drop and Create Proc SET @GenStr = ‘IF OBJECT_ID(’’p’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) + ‘’’)’+‘ISNOTNULL DROP PROC dbo.p’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) EXEC sp_executesql @GenStr SET @GenStr = ‘CREATE PROC dbo.p’ + RTRIM(REPLACE(@ClassName, ‘ ’, ‘’)) + ‘ (@ObjectID INT) AS SET NoCount ON ’ + @SQLStr EXEC sp_executesql @GenStr RETURN 679 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 680 Part IV Developing with SQL Server For additional examples of code generation, I recommend walking through the AutoAudit utility — it’s all code-generation that creates triggers, views, and user-defined functions. You can download the latest version from CodePlex.com. Preventing SQL Injection SQL injection is a hacker technique that appends SQL code to a parameter that is later executed as dynamic SQL. What makes SQL injection so dangerous is that anyone with access to the organization’s website who can enter data into a text field can attempt an SQL injection attack. There are several malicious techniques that involve appending code or modifying the WHERE clause. Before learning how to prevent it, it’s important to understand how it works, as the following sections explain. Appending malicious code Adding a statement terminator, another SQL command, and a comment, a hacker can pass code into the execute string. For example, if the parameter passed in is 123’; Delete OrderDetail the parameter, including the delete DDL command, placed within a dynamic SQL string would execute as a batch: SELECT * FROM Customers WHERE CustomerID = ‘123’; Delete OrderDetail ’ The statement terminator ends the intended code and the delete command looks to SQL Server like nothing more than the second line in the batch. The quotes would normally cause a syntax error, but the comment line solves that problem for the hacker. The result? An empty OrderDetail table. Other popular appended commands include running xp_commandshell or setting the sa password. Or 1=1 Another SQL injection technique is to modify the WHERE clause so that more rows are selected than intended. If the user enters the following string into the user text box: 123’ or 1=1 then the 1=1 (always true) condition is injected into the WHERE clause. The injected hyphens comment out the closing q uote: SELECT * FROM Customers WHERE CustomerID = ‘123’ or 1=1 ’ With every row selected by the SQL statement, what happens next depends on how the rest of the sys- tem handles multiple rows. Regardless, it’s not what should happen. 680 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 681 Dynamic SQL and Code Generation 29 Password? What password? Another creative use of SQL injection is to comment out part of the intended code. Suppose the user enters the following in the web form: UserName: Joe’ Password : who cares The resulting SQL statement might read as follows: SELECT USerID FROM Users WHERE UserName = ‘Joe’ ’ AND Password = ‘who cares’ The comment in the username causes SQL Server to ignore the rest of the WHERE clause, including the password condition. Preventing SQL Server injection attacks Several development techniques can prevent SQL injection: ■ Use EXECUTE AS and carefully define the roles so that statements don’t have permission to drop tables. ■ Use DRI referential integrity to prevent deleting primary table rows with dependent secondary table rows. ■ Never let user input mixed with dynamic SQL in a web form execute as submitted SQL. Always pass all parameters through a stored procedure. ■ Check for and reject parameters that include statement terminators, comments, or xp_. ■ Test your database using the SQL injection techniques described above. SQL injection is a real threat. If your application is exposed to entry from the Internet and you haven’t taken steps to prevent SQL injection, it’s only a matter of time before your database is attacked. Best Practice F or a flexible search procedure in Nordic, I’ve started using a new practice of parsing and joining. Rather than build a dynamic SQL WHERE clause, I allow the application to pass in a multiple-word search string. This is parsed into a table with each word becoming a row. The search table is then joined with the various data points that can be searched. For an example of this technique, turn back to Chapter 28, ‘‘Building Out the Data Abstraction Layer,’’ or download the latest version of Nordic from CodePlex.com. 681 www.getcoolebook.com . poorly. ■ T -SQL code that generates T -SQL code can be tricky to debug. 673 www.getcoolebook.com Nielsen c29.tex V4 - 07/23/2009 4:59pm Page 674 Part IV Developing with SQL Server Executing Dynamic SQL The. dynamic SQL IF @SQLWhere <> ‘’ SET @SQL = @SQL + ‘ where ’ + @SQLWhere + ‘;’; ∼∼Use this for testing and debug use only. PRINT @SQL; EXEC sp_executesql @SQL 676 www.getcoolebook.com Nielsen. 1 SET @SQLWhere = @SQLWhere + ‘ and ’; SET @SQLWhere = ‘ProductCategory = ’ + @ProductCategory; SET @NeedsAnd = 1; END; Assemble the select and the where portions of the dynamic SQL IF @SQLWhere