I'm creating a small extension to the JDBC API, with the hope of automating some common tasks and avoid boilerplate code.
One of its features will be a basic support for named parameters in prepared statements (that JDBC does not support natively).
Since this is a vital part of the library, I would appreciate your opinions about the module that:
- Parses the prepared statement, by replacing named parameters with question marks
- Creates a mapping between the name of the parameter and the index(es) of the resulting question mark(s)
For example, you have:
INSERT INTO customers (name, surname, age) VALUES (:name, :surname, :age)
The parse will produce this string (to be passed to JDBC standard APIs):
INSERT INTO customers (name, surname, age) VALUES (?, ?, ?)
and will also create a mapping between the parameters and the corresponding placeholders, which is:
name -> 1
surname -> 2
age -> 3
This is the whole parsing logic:
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class StatementParser {
private static final char PARAMETER_PLACEHOLDER = '?';
private static enum ParseStatus {
WAITING_FOR_IDENTIFIER, READING_IDENTIFIER
};
public static ParseResult parse(String sql, char marker) {
sql = sql + '\0'; // Insert null character at the end (for parsing)
ParseStatus status = ParseStatus.WAITING_FOR_IDENTIFIER;
Map<String, List<Integer>> parameters = new HashMap<String, List<Integer>>();
StringBuilder resultSql = new StringBuilder();
StringBuilder currentParameter = new StringBuilder();
int paramCount = 0;
for (int offset = 0; offset < sql.length(); offset++) {
char c = sql.charAt(offset);
switch (status) {
case WAITING_FOR_IDENTIFIER:
if (c == marker) {
status = ParseStatus.READING_IDENTIFIER;
} else {
resultSql.append(c);
}
break;
case READING_IDENTIFIER:
if (c == marker) {
throw new StatementParseError(
"Unexpected parameter marker at character " + offset);
} else if (Character.isLetterOrDigit(c)) {
currentParameter.append(c);
} else {
int paramNo = ++paramCount;
String paramName = currentParameter.toString();
if (paramName.isEmpty()) {
throw new StatementParseError(
"Error parsing parameter #" + paramNo);
}
if (parameters.containsKey(paramName)) {
parameters.get(paramName).add(paramNo);
} else {
List<Integer> list = new LinkedList<Integer>();
list.add(paramNo);
parameters.put(paramName, list);
}
resultSql.append(PARAMETER_PLACEHOLDER);
resultSql.append(c);
currentParameter.setLength(0); // reset buffer
status = ParseStatus.WAITING_FOR_IDENTIFIER;
}
break;
}
}
// Remove ending null character
resultSql.deleteCharAt(resultSql.length() - 1);
return new ParseResult(resultSql.toString(), parameters);
}
}
You can easily imagine what is the ParseResult
class about, without me posting it here. StatementParseError
is a RuntimeException
.
My doubts are the following:
- If a SQL string constant contains the
marker
character, this parser will not ignore it - whereas it should. Example:SELECT * FROM sometable WHERE foo = :param1 AND bar = 'a:b'
. I may try to improve my parser to consider also string delimiters (quotes) and ignore markers inside string constants, but each DBMS has it own rules when it comes, for example, to escape quotes inside strings. Do you think this shortcoming will seriously affect the robustness of my library, or I can just document it and suggest to use another marker as needed? - You may have noticed that I add a placeholder
\0
character at the end of the string, before parsing it. The reason is that if the input SQL string terminates with the last character of a named parameter identifier, then I need another iteration of the loop to find the end of the parameter identifier and store it in theparameters
map. Is there a cleaner way?