|
| 1 | +# Dynamic SQL |
| 2 | + |
| 3 | +Occasionally it is necessary to generate SQL dynamically. |
| 4 | + |
| 5 | +Rezoom.SQL permits running SQL statements specified fully at runtime, or adding |
| 6 | +SQL expressions at runtime into an otherwise-static query. These dynamic SQL |
| 7 | +expression do _not_ go through the RZSQL typechecker and translator, so they |
| 8 | +should be written directly for the dialect of SQL you are targeting with your |
| 9 | +[backend](../Configuration/Json.md#backend). |
| 10 | + |
| 11 | +You should avoid using dynamic SQL whenever possible, since it is easy to make |
| 12 | +mistakes. In particular, you must be careful to avoid SQL injection. |
| 13 | + |
| 14 | +Future additions to Rezoom.SQL are planned to reduce the number of situations in |
| 15 | +which dynamic SQL will be necessary and add safer APIs for common use cases like |
| 16 | +dynamic filtering and sorting on the output columns of a query. |
| 17 | + |
| 18 | +## How to use dynamic SQL |
| 19 | + |
| 20 | +The simplest way is to create a fully dynamic `Command`. You do this with the |
| 21 | +`dynamicCommand` function, which takes an array of "command fragments". You can |
| 22 | +get a command fragment representing a raw SQL string using the `sql` function, |
| 23 | +and a command fragment representing a parameter using `arg` or `argOfType`. |
| 24 | + |
| 25 | +An example will illustrate it better than any explanation I could write: |
| 26 | + |
| 27 | +```fsharp |
| 28 | +open Rezoom.SQL |
| 29 | +open Rezoom.SQL.Raw -- module with helpers for dynamic SQL |
| 30 | +
|
| 31 | +type ExampleQueryResult = |
| 32 | + { Id : int |
| 33 | + Name : string |
| 34 | + } |
| 35 | +
|
| 36 | +let exampleCommand (id : int) (nameSearch : string) = |
| 37 | + dynamicCommand<ExampleQueryResult> |
| 38 | + [| sql "SELECT Id, Name FROM USERS" |
| 39 | + sql " WHERE Id = " |
| 40 | + arg id |
| 41 | + sql " OR Name LIKE " |
| 42 | + arg ("%" + nameSearch + "%") |
| 43 | + |] |
| 44 | +``` |
| 45 | + |
| 46 | +## Adding dynamic SQL expressions to static SQL queries |
| 47 | + |
| 48 | +You can use the [erased function](Functions/README.md#erased-functions) |
| 49 | +`unsafe_inject_raw` on a parameter to pass dynamic SQL via that parameter at |
| 50 | +runtime. Again, an example will help: |
| 51 | + |
| 52 | +```fsharp |
| 53 | +open Rezoom.SQL |
| 54 | +open Rezoom.SQL.Raw |
| 55 | +
|
| 56 | +type MyMostlyStaticQuery = SQL<""" |
| 57 | + SELECT Id, Name FROM USERS |
| 58 | + WHERE unsafe_inject_raw(@dynSql) |
| 59 | +"""> |
| 60 | +
|
| 61 | +let exampleCommand (id : int) (nameSearch : string) = |
| 62 | + let exampleSql = |
| 63 | + [| sql "Id = " |
| 64 | + arg id |
| 65 | + sql " OR Name LIKE " |
| 66 | + arg ("%" + nameSearch + "%") |
| 67 | + |] |
| 68 | + MyMostlyStaticQuery.Command(dynSql = exampleSql) |
| 69 | +
|
| 70 | +``` |
| 71 | + |
| 72 | +Remember that dynamic SQL does not go through RZSQL translation so you'll need |
| 73 | +to use your backend's actual syntax. |
| 74 | + |
| 75 | +## Avoiding SQL injection in dynamic SQL |
| 76 | + |
| 77 | +**NEVER** pass inputs from an untrusted source (e.g. an end user) to the `sql` |
| 78 | +function. This allows that user to craft inputs that run whatever SQL they want. |
| 79 | +This is called [SQL injection](https://www.google.com/search?q=sql+injection) |
| 80 | +and is one of the worst vulnerabilities an application can have. It combines |
| 81 | +potentially devastating consequences with easy exploitation. |
| 82 | + |
| 83 | +To avoid SQL injection, **ALWAYS** pass user inputs as SQL parameters using the |
| 84 | +`arg` or `argOfType` functions. `arg` will attempt to guess the `DbType` of the |
| 85 | +given value based on its .NET type, while `argOfType` has you specify it |
| 86 | +yourself. |
| 87 | + |
| 88 | +### WRONG |
| 89 | + |
| 90 | +```fsharp |
| 91 | +
|
| 92 | +// DO NOT do it this way! |
| 93 | +
|
| 94 | +let exampleCommand (id : int) (nameSearch : string) = |
| 95 | + dynamicCommand<ExampleQueryResult> |
| 96 | + [| sql "SELECT Id, Name FROM USERS" |
| 97 | + sql " WHERE Name LIKE " |
| 98 | +
|
| 99 | + // BAD. DO NOT DO THIS! |
| 100 | + sql ("'%" + nameSearch + "%'") // <-- NO! |
| 101 | + |] |
| 102 | +
|
| 103 | +``` |
| 104 | + |
| 105 | +### RIGHT |
| 106 | + |
| 107 | +```fsharp |
| 108 | +
|
| 109 | +// Use a parameter to pass the untrusted user input. |
| 110 | +
|
| 111 | +// Notice that the percent signs aren't surrounded with single quotes anymore, |
| 112 | +// because we no longer need to (badly) attempt to convert the input string to |
| 113 | +// SQL source code. |
| 114 | +
|
| 115 | +let exampleCommand (id : int) (nameSearch : string) = |
| 116 | + dynamicCommand<ExampleQueryResult> |
| 117 | + [| sql "SELECT Id, Name FROM USERS" |
| 118 | + sql " WHERE Name LIKE " |
| 119 | +
|
| 120 | + // GOOD: use `arg` function to make a bind-parameter at runtime |
| 121 | + arg ("%" + nameSearch + "%") |
| 122 | + |] |
| 123 | +
|
| 124 | +``` |
0 commit comments