Repository navigation
Expand file tree
/
Copy pathSqlParameterCompiler.java
More file actions
276 lines (228 loc) · 9.7 KB
/
Copy pathSqlParameterCompiler.java
File metadata and controls
276 lines (228 loc) · 9.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package dev.sqlcj.sql;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcNamedParameter;
import net.sf.jsqlparser.parser.CCJSqlParserConstants;
import net.sf.jsqlparser.parser.Node;
import net.sf.jsqlparser.parser.Token;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* Compiles the positional {@code $N} and named {@code :name} parameters of a
* parsed SQL source into JDBC {@code ?} markers.
*
* <p>Only tokens that the SQL parser itself reported as parameter tokens are
* replaced. Every other character is copied from the source, so placeholder
* text inside string literals, quoted identifiers, comments, and identifiers
* stays byte-for-byte unchanged.
*
* <p>Each distinct placeholder name is one logical parameter, numbered by its
* first textual occurrence, and every occurrence of that name becomes a
* {@code ?} of its own. A placeholder form this compiler does not recognize,
* such as a qualified, quoted, or {@code &name} placeholder, stays in the SQL
* and is reported as written for semantic analysis to reject, as is an
* anonymous {@code ?} placeholder, which has no parameter to bind.
*/
final class SqlParameterCompiler {
/**
* A positional parameter image. The digits are bounded because an index
* outside the {@code int} range is not a parameter this compiler can
* report; such a token stays in the SQL and is rejected later as an
* unaccounted parameter.
*/
private static final Pattern POSITIONAL_PARAMETER = Pattern.compile("\\$\\d{1,9}");
/**
* A named parameter name, which is an unquoted identifier of ASCII letters,
* digits, and underscores that does not start with a digit. Names are
* compared exactly as written.
*/
private static final Pattern PARAMETER_NAME = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
/**
* The image of the token that opens a named parameter. The parser reports
* the cast operator {@code ::} as a token of its own, so only a single
* colon opens a name.
*/
private static final String NAME_PREFIX = ":";
/**
* The image of an anonymous placeholder token. The parser reports it as a
* token of its own, so a {@code ?} inside a string literal, a quoted
* identifier, or a comment is not reported here, and the JSON operators
* {@code ?|} and {@code ?&} are reported with their own images.
*/
private static final String ANONYMOUS_PARAMETER = "?";
/**
* {@link Token#absoluteBegin} and {@link Token#absoluteEnd} count the first
* source character as position one, so a source offset is one less than the
* reported position.
*/
private static final int SOURCE_OFFSET = 1;
SqlParameters compile(String sql, Node astRoot) {
if (astRoot == null) {
throw new SqlParseException("SQL parse tree is unavailable.");
}
Token firstToken = astRoot.jjtGetFirstToken();
Token lastToken = astRoot.jjtGetLastToken();
if (firstToken == null || lastToken == null) {
throw new SqlParseException("SQL parse tree has no tokens.");
}
StringBuilder executableSql = new StringBuilder();
List<Integer> indexes = new ArrayList<>();
List<String> names = new ArrayList<>();
boolean anonymous = false;
boolean positional = false;
int copied = 0;
for (Token token = firstToken; token != null; token = token.next) {
if (isAnonymousParameter(token)) {
anonymous = true;
}
if (isPositionalParameter(token)) {
int begin = requireSpan(
sql,
token.image,
token.absoluteBegin,
token.absoluteEnd,
copied
);
executableSql
.append(sql, copied, begin)
.append('?');
copied = begin + token.image.length();
indexes.add(Integer.parseInt(token.image.substring(1)));
positional = true;
} else if (token != lastToken && isNamedParameter(token, token.next)) {
Token name = token.next;
String image = token.image + name.image;
int begin = requireSpan(
sql,
image,
token.absoluteBegin,
name.absoluteEnd,
copied
);
executableSql
.append(sql, copied, begin)
.append('?');
copied = begin + image.length();
indexes.add(numberOf(name.image, names));
token = name;
}
if (token == lastToken) {
break;
}
}
executableSql.append(sql, copied, sql.length());
List<String> uncompiled = new ArrayList<>();
collectUncompiledPlaceholders(astRoot, names, uncompiled);
return new SqlParameters(
executableSql.toString(),
indexes,
names,
uncompiled,
positional,
anonymous
);
}
/**
* Collects the named placeholders of the parse tree that this compiler did
* not replace, written as the source spells them, so that an accepted query
* never keeps such a placeholder in its executable SQL.
*
* <p>The parser gives the operand of a {@code ::} cast no parse-tree node
* of its own, so a cast reports the placeholder it casts, through any
* further cast of the same operand.
*/
private void collectUncompiledPlaceholders(Node node, List<String> names, List<String> uncompiled) {
collectUncompiledPlaceholder(node.jjtGetValue(), names, uncompiled);
if (node.jjtGetValue() instanceof CastExpression cast) {
collectUncompiledPlaceholder(castOperand(cast), names, uncompiled);
}
for (int child = 0; child < node.jjtGetNumChildren(); child++) {
collectUncompiledPlaceholders(node.jjtGetChild(child), names, uncompiled);
}
}
/** The operand a cast casts, which is the operand of a chained cast. */
private Expression castOperand(CastExpression cast) {
Expression operand = cast.getLeftExpression();
return operand instanceof CastExpression chained
? castOperand(chained)
: operand;
}
/**
* Collects one parse-tree value when it is a named placeholder this
* compiler did not replace, once per placeholder as the source spells it.
*/
private void collectUncompiledPlaceholder(Object value, List<String> names, List<String> uncompiled) {
if (!(value instanceof JdbcNamedParameter named) || isCompiled(named, names)) {
return;
}
String placeholder = named.getParameterCharacter() + named.getName();
if (!uncompiled.contains(placeholder)) {
uncompiled.add(placeholder);
}
}
/**
* Reports whether a named placeholder the parser produced is one this
* compiler replaced, which requires both its colon and its name to be the
* supported form.
*/
private boolean isCompiled(JdbcNamedParameter named, List<String> names) {
return NAME_PREFIX.equals(named.getParameterCharacter())
&& names.contains(named.getName());
}
private boolean isAnonymousParameter(Token token) {
return ANONYMOUS_PARAMETER.equals(token.image);
}
private boolean isPositionalParameter(Token token) {
return token.kind == CCJSqlParserConstants.S_PARAMETER
&& token.image != null
&& POSITIONAL_PARAMETER.matcher(token.image).matches();
}
/**
* Reports whether a colon token and the token after it are one named
* parameter, which requires the name to follow the colon directly. A name
* separated from its colon, a qualified or quoted name, and a name that is
* not an unquoted identifier are other text, which stays in the SQL.
*/
private boolean isNamedParameter(Token colon, Token name) {
return colon.kind == CCJSqlParserConstants.DOUBLE_COLON
&& NAME_PREFIX.equals(colon.image)
&& name != null
&& name.image != null
&& name.absoluteBegin == colon.absoluteEnd
&& PARAMETER_NAME.matcher(name.image).matches();
}
/**
* Reports the logical parameter number of a placeholder name: the number
* the name already has, or the next number, which records the name so that
* distinct names are numbered by their first occurrence.
*/
private int numberOf(String name, List<String> names) {
int index = names.indexOf(name);
if (index >= 0) {
return index + 1;
}
names.add(name);
return names.size();
}
/**
* Returns the source offset of a parameter after requiring that its
* reported span follows the previous replacement, stays inside the source,
* and holds exactly the parameter image.
*/
private int requireSpan(String sql, String image, int absoluteBegin, int absoluteEnd, int copied) {
int begin = absoluteBegin - SOURCE_OFFSET;
int end = absoluteEnd - SOURCE_OFFSET;
boolean valid = begin >= copied
&& end <= sql.length()
&& end - begin == image.length()
&& sql.startsWith(image, begin);
if (!valid) {
throw new SqlParseException(
"Parameter '%s' reported an unusable source position: [%d, %d)"
.formatted(image, begin, end)
);
}
return begin;
}
}