1
1
module SqlStrings
2
2
3
- export @sql
3
+ export @sql_cmd
4
4
5
5
"""
6
6
Literal(str)
7
7
8
- Literal string argument to `@sql `. These are literal query fragments of SQL
8
+ Literal string argument to `@sql_cmd `. These are literal query fragments of SQL
9
9
source text.
10
10
"""
11
11
struct Literal
@@ -19,8 +19,9 @@ Literal(val) = Literal(string(val))
19
19
"""
20
20
Sql
21
21
22
- A query or query-fragment which keeps track of interpolations and will pass
23
- them as SQL query parameters. Construct this type with the `@sql` macro.
22
+ An SQL query or query-fragment which keeps track of interpolations and will
23
+ pass them as SQL query parameters. Construct this type with the `@sql_cmd`
24
+ macro.
24
25
"""
25
26
struct Sql
26
27
args:: Vector
@@ -54,12 +55,77 @@ function process_args!(processed, splat::SplatArgs, args...)
54
55
return process_args! (processed, args... )
55
56
end
56
57
58
+ function parse_interpolations (str, allow_dollars_in_strings)
59
+ args = []
60
+ i = 1
61
+ literal_start = i
62
+ literal_end = 0
63
+ in_singlequote = false
64
+ prev_was_backslash = false
65
+ while i <= lastindex (str)
66
+ c = str[i]
67
+ if ! allow_dollars_in_strings && in_singlequote && c == ' $'
68
+ error (""" Interpolated arguments should not be quoted, but found quoting in sql`$str `
69
+ subexpression starting at `$(str[i: end ]) `""" )
70
+ end
71
+ if c == ' $' && ! in_singlequote
72
+ if prev_was_backslash
73
+ literal_end = prevind (str, literal_end, 1 )
74
+ if literal_start <= literal_end
75
+ push! (args, Literal (str[literal_start: literal_end]))
76
+ end
77
+ literal_start = i
78
+ i = nextind (str, i)
79
+ else
80
+ if literal_start <= literal_end
81
+ push! (args, Literal (str[literal_start: literal_end]))
82
+ end
83
+ (interpolated_arg, i) = Meta. parse (str, i+ 1 ; greedy= false )
84
+ if Meta. isexpr (interpolated_arg, :... )
85
+ push! (args, :(SplatArgs ($ (esc (interpolated_arg. args[1 ])))))
86
+ else
87
+ push! (args, esc (interpolated_arg))
88
+ end
89
+ literal_start = i
90
+ end
91
+ else
92
+ if c == ' \' '
93
+ # We assume standard SQL which uses '' rather than \' for
94
+ # escaping quotes.
95
+ in_singlequote = ! in_singlequote
96
+ end
97
+ literal_end = i
98
+ i = nextind (str, i)
99
+ end
100
+ prev_was_backslash = c == ' \\ '
101
+ end
102
+ if literal_start <= literal_end
103
+ push! (args, Literal (str[literal_start: literal_end]))
104
+ end
105
+ args
106
+ end
107
+
108
+ """
109
+ allow_dollars_in_strings[] = true
110
+
111
+ Set this parsing option to `false` to disallow dollars inside SQL strings, for
112
+ example disallowing the `'\$ s'` in
113
+
114
+ sql`select * from foo where s = '\$ s'`
115
+
116
+ When converting code from plain string-based interpolation, it's helpful to
117
+ have a macro-expansion-time sanity check that no manual quoting of interpolated
118
+ arguments remains.
119
+ """
120
+ const allow_dollars_in_strings = Ref (true )
121
+
57
122
"""
58
123
sql`SOME SQL ... \$ var`
59
124
sql`SOME SQL ... \$ (var...)`
125
+ sql`SOME SQL ... 'A \$ literal string'`
60
126
sql``
61
127
62
- The `@sql ` macro is a tool for tracking SQL query strings together with
128
+ The `@sql_cmd ` macro is a tool for tracking SQL query strings together with
63
129
their parameters, but without interpolating the parameters into the query
64
130
string directly. Instead, interpolations like `\$ x` will result in the value of
65
131
`x` being passed as a query parameter. If you've got a collection of values to
@@ -68,69 +134,53 @@ within the interpolation, for example `insert into foo values(\$(x...))`.
68
134
69
135
Use this rather than direct string interpolation to prevent SQL injection
70
136
attacks and allow systematic conversion of Julia types into their SQL
71
- equivalents.
137
+ equivalents via the database layer, rather than via `string()` .
72
138
73
139
Empty query fragments can be generated with ```sql`` ``` which is useful if you
74
- must dynamically generate SQL code based on conditionals (however also consider
75
- embedding any conditionals on the SQL side rather than in the Julia code.)
140
+ must dynamically generate SQL code based on conditionals. However you should
141
+ also consider embedding any conditionals on the SQL side rather than in the
142
+ Julia code.
143
+
144
+ *Interpolations are ignored* inside standard SQL Strings with a single quote,
145
+ so using `'A \$ literal string'` will include `\$ literal` rather than the value
146
+ of the variable `literal`. If converting code from using raw strings, you may
147
+ have needed to quote interpolations. In that case you can check your conversion
148
+ by setting `SqlStrings.allow_dollars_in_strings[] = false`.
149
+
150
+ If you need to include a literal `\$ ` in the SQL code outside a string, you can
151
+ escape it with `\\\$ `.
76
152
"""
77
- macro sql (ex)
78
- if ex isa String
79
- args = [Literal (ex)]
80
- elseif ex isa Expr && ex. head == :string
81
- args = []
82
- for (i,arg) in enumerate (ex. args)
83
- if arg isa String
84
- push! (args, Literal (arg))
85
- else
86
- # Sanity check: arguments should not be quoted
87
- prev_quote = i > 1 && ex. args[i- 1 ] isa String && endswith (ex. args[i- 1 ], ' \' ' )
88
- next_quote = i < length (ex. args) && ex. args[i+ 1 ] isa String && startswith (ex. args[i+ 1 ], ' \' ' )
89
- if prev_quote || next_quote
90
- error (""" Interpolated arguments should not be quoted, but found quoting in subexpression
91
- $(Expr (:string , ex. args[i- 1 : i+ 1 ]. .. )) """ )
92
- end
93
- if Meta. isexpr (arg, :... )
94
- push! (args, :(SplatArgs ($ (esc (arg. args[1 ])))))
95
- else
96
- push! (args, esc (arg))
97
- end
98
- end
99
- end
100
- else
101
- error (" Unexpected expression passed to @sql: `$ex `" )
102
- end
153
+ macro sql_cmd (str)
154
+ args = parse_interpolations (str, allow_dollars_in_strings[])
103
155
quote
104
156
Sql (process_args! ([], $ (args... )))
105
157
end
106
158
end
107
159
108
- macro sql ()
109
- Sql ([])
110
- end
111
-
112
160
function Base.:* (x:: Sql , y:: Sql )
113
161
Sql (vcat (x. args, [Literal (" " )], y. args))
114
162
end
115
163
116
- function _prepare (query:: Sql )
164
+ default_placeholder_string (i) = " \$ $i "
165
+
166
+ function prepare (sql:: Sql , to_placeholder = default_placeholder_string)
117
167
querystr = " "
118
168
arg_values = []
119
169
i = 1
120
- for arg in query . args
170
+ for arg in sql . args
121
171
if arg isa Literal
122
172
querystr *= arg. fragment
123
173
else
124
- querystr *= " \$ $i "
174
+ querystr *= to_placeholder (i)
125
175
push! (arg_values, arg)
126
176
i += 1
127
177
end
128
178
end
129
179
querystr, arg_values
130
180
end
131
181
132
- function Base. show (io:: IO , query :: Sql )
133
- query, arg_values = _prepare (query )
182
+ function Base. show (io:: IO , sql :: Sql )
183
+ query, arg_values = prepare (sql )
134
184
print (io, query)
135
185
if ! isempty (arg_values)
136
186
args_str = join ([" \$ $i = $(repr (val)) " for (i,val) in enumerate (arg_values)], " \n " )
0 commit comments