com.mysql.cj.QueryInfo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mysql-connector-java Show documentation
Show all versions of mysql-connector-java Show documentation
JDBC Type 4 driver for MySQL
The newest version!
/*
* Copyright (c) 2017, 2022, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 2.0, as published by the
* Free Software Foundation.
*
* This program is also distributed with certain software (including but not
* limited to OpenSSL) that is licensed under separate terms, as designated in a
* particular file or component or in included license documentation. The
* authors of MySQL hereby grant you an additional permission to link the
* program and your derivative works with the separately licensed software that
* they have included with MySQL.
*
* Without limiting anything contained in the foregoing, this file, which is
* part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
* version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
* for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.mysql.cj;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.util.SearchMode;
import com.mysql.cj.util.StringInspector;
import com.mysql.cj.util.StringUtils;
/**
* Represents the "parsed" state of a prepared query, with the statement broken up into its static and dynamic (where parameters are bound) parts.
*/
public class QueryInfo {
private static final String OPENING_MARKERS = "`'\"";
private static final String CLOSING_MARKERS = "`'\"";
private static final String OVERRIDING_MARKERS = "";
private static final String INSERT_STATEMENT = "INSERT";
private static final String REPLACE_STATEMENT = "REPLACE";
private static final String VALUE_CLAUSE = "VALUE";
private static final String AS_CLAUSE = "AS";
private static final String[] ODKU_CLAUSE = new String[] { "ON", "DUPLICATE", "KEY", "UPDATE" };
private static final String LAST_INSERT_ID_FUNC = "LAST_INSERT_ID";
private QueryInfo baseQueryInfo = null;
private String sql;
private String encoding;
private QueryReturnType queryReturnType = null;
private int queryLength = 0;
private int queryStartPos = 0;
private char statementFirstChar = Character.MIN_VALUE;
private int batchCount = 1;
private int numberOfPlaceholders = 0;
private int numberOfQueries = 0;
private boolean containsOnDuplicateKeyUpdate = false;
private boolean isRewritableWithMultiValuesClause = false;
private int valuesClauseLength = -1;
private ArrayList valuesEndpoints = new ArrayList<>();
private byte[][] staticSqlParts = null;
/**
* Constructs a {@link QueryInfo} object for the given query or multi-query. The parsed result of this query allows to determine the location of the
* placeholders, the query static parts and whether this query can be rewritten as a multi-values clause query.
*
* @param sql
* the query SQL string to parse and analyze
* @param session
* the {@link Session} under which the query analysis must be done.
* @param encoding
* the characters encoding to use when extracting the query static parts as byte arrays.
*/
public QueryInfo(String sql, Session session, String encoding) {
if (sql == null) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("QueryInfo.NullSql"), session.getExceptionInterceptor());
}
this.baseQueryInfo = this;
this.sql = sql;
this.encoding = encoding;
boolean noBackslashEscapes = session.getServerSession().isNoBackslashEscapesSet();
boolean rewriteBatchedStatements = session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue();
boolean dontCheckOnDuplicateKeyUpdateInSQL = session.getPropertySet().getBooleanProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL).getValue();
this.queryReturnType = getQueryReturnType(this.sql, noBackslashEscapes);
this.queryLength = this.sql.length();
StringInspector strInspector = new StringInspector(this.sql, OPENING_MARKERS, CLOSING_MARKERS, OVERRIDING_MARKERS,
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__BSE_MRK_COM_MYM_HNT_WS);
// Skip comments at the beginning of queries.
this.queryStartPos = strInspector.indexOfNextAlphanumericChar();
if (this.queryStartPos == -1) {
this.queryStartPos = this.queryLength;
} else {
this.numberOfQueries = 1;
this.statementFirstChar = Character.toUpperCase(strInspector.getChar());
}
// Only INSERT and REPLACE statements support multi-values clause rewriting.
boolean isInsert = strInspector.matchesIgnoreCase(INSERT_STATEMENT) != -1;
if (isInsert) {
strInspector.incrementPosition(INSERT_STATEMENT.length()); // Advance to the end of "INSERT".
}
boolean isReplace = !isInsert && strInspector.matchesIgnoreCase(REPLACE_STATEMENT) != -1;
if (isReplace) {
strInspector.incrementPosition(REPLACE_STATEMENT.length()); // Advance to the end of "REPLACE".
}
// Check if the statement has potential to be rewritten as a multi-values clause statement, i.e., if it is an INSERT or REPLACE statement and
// 'rewriteBatchedStatements' is enabled.
boolean rewritableAsMultiValues = (isInsert || isReplace) && rewriteBatchedStatements;
// Check if should look for ON DUPLICATE KEY UPDATE CLAUSE, i.e., if it is an INSERT statement and `dontCheckOnDuplicateKeyUpdateInSQL` is disabled.
boolean lookForInDuplicateKeyUpdate = isInsert && !dontCheckOnDuplicateKeyUpdateInSQL;
// Scan placeholders.
int generalEndpointStart = 0;
int valuesEndpointStart = 0;
int valuesClauseBegin = -1;
int valuesClauseEnd = -1;
boolean withinValuesClause = false;
int parensLevel = 0;
int matchEnd = -1;
// Endpoints for the satement's static sections (parts around placeholders).
ArrayList staticEndpoints = new ArrayList<>();
while (strInspector.indexOfNextChar() != -1) {
if (strInspector.getChar() == '?') {
this.numberOfPlaceholders++;
int endpointEnd = strInspector.getPosition();
staticEndpoints.add(generalEndpointStart);
staticEndpoints.add(endpointEnd);
strInspector.incrementPosition();
generalEndpointStart = strInspector.getPosition(); // Next section starts after the placeholder.
if (rewritableAsMultiValues) {
if (valuesClauseBegin == -1) { // There's a placeholder before the VALUES clause.
rewritableAsMultiValues = false;
} else if (valuesClauseEnd != -1) { // There's a placeholder after the end of the VALUES clause.
rewritableAsMultiValues = false;
} else if (withinValuesClause) {
this.valuesEndpoints.add(valuesEndpointStart);
this.valuesEndpoints.add(endpointEnd);
valuesEndpointStart = generalEndpointStart;
}
}
} else if (strInspector.getChar() == ';') {
strInspector.incrementPosition();
if (strInspector.indexOfNextNonWsChar() != -1) {
this.numberOfQueries++;
if (rewritableAsMultiValues) {
rewritableAsMultiValues = false;
valuesClauseBegin = -1;
valuesClauseEnd = -1;
withinValuesClause = false;
parensLevel = 0;
}
// Check if continue looking for ON DUPLICATE KEY UPDATE.
if (dontCheckOnDuplicateKeyUpdateInSQL || this.containsOnDuplicateKeyUpdate) {
lookForInDuplicateKeyUpdate = false;
} else {
isInsert = strInspector.matchesIgnoreCase(INSERT_STATEMENT) != -1;
if (isInsert) {
strInspector.incrementPosition(INSERT_STATEMENT.length()); // Advance to the end of "INSERT".
}
lookForInDuplicateKeyUpdate = isInsert;
}
}
} else if (rewritableAsMultiValues || lookForInDuplicateKeyUpdate) {
if (valuesClauseBegin == -1 && strInspector.matchesIgnoreCase(VALUE_CLAUSE) != -1) { // VALUE(S) clause found.
strInspector.incrementPosition(VALUE_CLAUSE.length()); // Advance to the end of "VALUE".
if (strInspector.matchesIgnoreCase("S") != -1) { // Check for the "S" in "VALUE(S)" and advance 1 more character if needed.
strInspector.incrementPosition();
}
withinValuesClause = true;
strInspector.indexOfNextChar(); // Position on the first values list character.
valuesClauseBegin = strInspector.getPosition();
if (rewritableAsMultiValues) {
valuesEndpointStart = valuesClauseBegin;
}
} else if (withinValuesClause && strInspector.getChar() == '(') {
parensLevel++;
strInspector.incrementPosition();
} else if (withinValuesClause && strInspector.getChar() == ')') {
parensLevel--;
if (parensLevel < 0) {
parensLevel = 0; // Keep going, not checking for syntax validity.
}
strInspector.incrementPosition();
valuesClauseEnd = strInspector.getPosition(); // It may not be the end of the VALUES clause yet but save it for later.
} else if (withinValuesClause && parensLevel == 0 && isInsert && strInspector.matchesIgnoreCase(AS_CLAUSE) != -1) { // End of VALUES clause.
if (valuesClauseEnd == -1) {
valuesClauseEnd = strInspector.getPosition();
}
withinValuesClause = false;
strInspector.incrementPosition(AS_CLAUSE.length()); // Advance to the end of "AS".
if (rewritableAsMultiValues) {
this.valuesEndpoints.add(valuesEndpointStart);
this.valuesEndpoints.add(valuesClauseEnd);
}
} else if (withinValuesClause && parensLevel == 0 && isInsert && (matchEnd = strInspector.matchesIgnoreCase(ODKU_CLAUSE)) != -1) { // End of VALUES clause.
if (valuesClauseEnd == -1) {
valuesClauseEnd = strInspector.getPosition();
}
withinValuesClause = false;
lookForInDuplicateKeyUpdate = false;
this.containsOnDuplicateKeyUpdate = true;
strInspector.incrementPosition(matchEnd - strInspector.getPosition()); // Advance to the end of "ON DUPLICATE KEY UPDATE".
if (rewritableAsMultiValues) {
this.valuesEndpoints.add(valuesEndpointStart);
this.valuesEndpoints.add(valuesClauseEnd);
}
} else if (rewritableAsMultiValues && valuesClauseBegin != -1 && strInspector.matchesIgnoreCase(LAST_INSERT_ID_FUNC) != -1) {
rewritableAsMultiValues = false;
strInspector.incrementPosition(LAST_INSERT_ID_FUNC.length()); // Advance to the end of "LAST_INSERT_ID".
} else {
strInspector.incrementPosition();
}
} else {
strInspector.incrementPosition();
}
}
staticEndpoints.add(generalEndpointStart);
staticEndpoints.add(this.queryLength);
if (rewritableAsMultiValues) {
if (withinValuesClause) {
withinValuesClause = false;
this.valuesEndpoints.add(valuesEndpointStart);
this.valuesEndpoints.add(valuesClauseEnd != -1 ? valuesClauseEnd : this.queryLength);
}
if (valuesClauseBegin != -1) {
this.valuesClauseLength = (valuesClauseEnd != -1 ? valuesClauseEnd : this.queryLength) - valuesClauseBegin;
} else {
rewritableAsMultiValues = false;
}
} else {
this.valuesEndpoints.clear();
}
this.isRewritableWithMultiValuesClause = rewritableAsMultiValues;
this.staticSqlParts = new byte[this.numberOfPlaceholders + 1][];
for (int i = 0, j = 0; i <= this.numberOfPlaceholders; i++) {
int begin = staticEndpoints.get(j++);
int end = staticEndpoints.get(j++);
int length = end - begin;
this.staticSqlParts[i] = StringUtils.getBytes(this.sql, begin, length, this.encoding);
}
}
/**
* Constructs a {@link QueryInfo} object with a multi-value clause for the specified batch count, that stems from the specified baseQueryInfo.
*
* @param baseQueryInfo
* the {@link QueryInfo} instance that provides the query static parts used to create the new instance now augmented to accommodate the extra
* number of parameters
* @param batchCount
* the number of batches, i.e., the number of times the VALUES clause needs to be repeated inside the new query
*/
private QueryInfo(QueryInfo baseQueryInfo, int batchCount) {
this.baseQueryInfo = baseQueryInfo;
this.sql = null;
this.encoding = this.baseQueryInfo.encoding;
this.queryReturnType = this.baseQueryInfo.queryReturnType;
this.queryLength = 0;
this.queryStartPos = this.baseQueryInfo.queryStartPos;
this.statementFirstChar = this.baseQueryInfo.statementFirstChar;
this.batchCount = batchCount;
this.numberOfPlaceholders = this.baseQueryInfo.numberOfPlaceholders * this.batchCount;
this.numberOfQueries = 1;
this.containsOnDuplicateKeyUpdate = this.baseQueryInfo.containsOnDuplicateKeyUpdate;
this.isRewritableWithMultiValuesClause = true;
this.valuesClauseLength = -1;
if (this.numberOfPlaceholders == 0) {
this.staticSqlParts = new byte[1][];
int begin = this.baseQueryInfo.valuesEndpoints.get(0);
int end = this.baseQueryInfo.valuesEndpoints.get(1);
int length = end - begin;
byte[] valuesSegment = StringUtils.getBytes(this.baseQueryInfo.sql, begin, length, this.encoding);
byte[] bindingSegment = StringUtils.getBytes(",", this.encoding);
// First batch is in the query.
ByteBuffer queryByteBuffer = ByteBuffer.allocate(this.baseQueryInfo.queryLength + (length + bindingSegment.length) * (batchCount - 1));
// Head section: from the start to the end of the values - includes first values batch.
queryByteBuffer.put(StringUtils.getBytes(this.baseQueryInfo.sql, 0, this.baseQueryInfo.valuesEndpoints.get(1), this.encoding));
// Values section: repeat as many times as needed to complete the requested batch count.
for (int i = 0; i < this.batchCount - 1; i++) {
// Add the segment that binds two batches followed by the next batch.
queryByteBuffer.put(bindingSegment);
queryByteBuffer.put(valuesSegment);
}
// Tail section: from the end of values until the end.
begin = this.baseQueryInfo.valuesEndpoints.get(1);
end = this.baseQueryInfo.queryLength;
length = end - begin;
queryByteBuffer.put(StringUtils.getBytes(this.baseQueryInfo.sql, begin, length, this.encoding));
this.staticSqlParts[0] = queryByteBuffer.array();
} else {
this.staticSqlParts = new byte[this.numberOfPlaceholders + 1][];
// Build the values binding segment: [values_end][comma][values_begin], e.g., "),(".
int begin = this.baseQueryInfo.valuesEndpoints.get(this.baseQueryInfo.valuesEndpoints.size() - 2);
int end = this.baseQueryInfo.valuesEndpoints.get(this.baseQueryInfo.valuesEndpoints.size() - 1);
int length = end - begin;
byte[] valuesEndSegment = StringUtils.getBytes(this.baseQueryInfo.sql, begin, length, this.encoding);
byte[] delimiter = StringUtils.getBytes(",", this.encoding);
begin = this.baseQueryInfo.valuesEndpoints.get(0);
end = this.baseQueryInfo.valuesEndpoints.get(1);
length = end - begin;
byte[] valuesBeginSegment = StringUtils.getBytes(this.baseQueryInfo.sql, begin, length, this.encoding);
ByteBuffer bindingSegmentByteBuffer = ByteBuffer.allocate(valuesEndSegment.length + delimiter.length + valuesBeginSegment.length);
bindingSegmentByteBuffer.put(valuesEndSegment).put(delimiter).put(valuesBeginSegment);
byte[] bindingSegment = bindingSegmentByteBuffer.array();
// Head section: same as in the original query.
this.staticSqlParts[0] = this.baseQueryInfo.staticSqlParts[0];
// Values section: repeat as many times as the requested batch count.
for (int i = 0, p = 1; i < this.batchCount; i++, p++) {
for (int j = 1; j < this.baseQueryInfo.staticSqlParts.length - 1; j++, p++) {
this.staticSqlParts[p] = this.baseQueryInfo.staticSqlParts[j];
}
// Add the segment that binds two batches.
this.staticSqlParts[p] = bindingSegment;
}
// Tail section: same as in the original query.
this.staticSqlParts[this.staticSqlParts.length - 1] = this.baseQueryInfo.staticSqlParts[this.baseQueryInfo.staticSqlParts.length - 1];
}
}
/**
* Returns the number of queries identified in the original SQL string. Different queries are identified by the presence of the query delimiter character,
* i.e., a semicolon.
*
* @return the number of queries identified in the original SQL string
*/
public int getNumberOfQueries() {
return this.numberOfQueries;
}
/**
* Returns the return type of the parsed query. This operation does not take into consideration the multiplicity of queries in the specified SQL.
*
* @return the return type of the parsed query
*/
public QueryReturnType getQueryReturnType() {
return this.queryReturnType;
}
/**
* Returns the first character of the statement from the query used to build this {@link QueryInfo}.
*
* @return the first character of the statement
*/
public char getFirstStmtChar() {
/* TODO: First char based logic is questionable. Consider replacing by statement check. */
return this.baseQueryInfo.statementFirstChar;
}
/**
* If this object represents a query that is re-writable as a multi-values statement and if rewriting batched statements is enabled, then returns the
* length of the parsed VALUES clause section, including the placeholder characters themselves, otherwise returns -1.
*
* @return the length of the parsed VALUES clause section, including the placeholder characters themselves, otherwise returns -1
*/
public int getValuesClauseLength() {
return this.baseQueryInfo.valuesClauseLength;
}
/**
* Does this query info represent a query that contains an ON DUPLICATE KEY UPDATE clause? This operation does not take into consideration the multiplicity
* of queries in the original SQL.
*
* Checking whether the original query contains an ON DUPLICATE KEY UPDATE clause is conditional to how the connection properties
* `dontCheckOnDuplicateKeyUpdateInSQL` and `rewriteBatchedStatements` are set, with `rewriteBatchedStatements=true` implicitly disabling
* `dontCheckOnDuplicateKeyUpdateInSQL`.
*
* @return true
if the query or any of the original queries contain an ON DUPLICATE KEY UPDATE clause.
*/
public boolean containsOnDuplicateKeyUpdate() {
return this.containsOnDuplicateKeyUpdate;
}
/**
* Returns the static sections of the parsed query, as byte arrays, split by the places where the placeholders were located.
*
* @return the static sections of the parsed query, as byte arrays, split by the places where the placeholders were located
*/
public byte[][] getStaticSqlParts() {
return this.staticSqlParts;
}
/**
* Can this query be rewritten as a multi-values clause?
*
* @return true
if the query can be rewritten as a multi-values query.
*/
public boolean isRewritableWithMultiValuesClause() {
return this.isRewritableWithMultiValuesClause;
}
/**
* Returns a {@link QueryInfo} for a multi-values INSERT/REPLACE assembled for the specified batch count, without re-parsing.
*
* @param count
* the number of parameter batches
* @return {@link QueryInfo}
*/
public QueryInfo getQueryInfoForBatch(int count) {
if (count == 1) {
return this.baseQueryInfo;
}
if (count == this.batchCount) {
return this;
}
if (!this.isRewritableWithMultiValuesClause) {
return null;
}
return new QueryInfo(this.baseQueryInfo, count);
}
/**
* Returns a preparable query for the batch count of this {@link QueryInfo}.
*
* @return
* a preparable query string with the appropriate number of placeholders
*/
public String getSqlForBatch() {
if (this.batchCount == 1) {
return this.baseQueryInfo.sql;
}
int size = this.baseQueryInfo.queryLength + (this.batchCount - 1) * this.baseQueryInfo.valuesClauseLength + this.batchCount - 1;
StringBuilder buf = new StringBuilder(size);
buf.append(StringUtils.toString(this.staticSqlParts[0], this.encoding));
for (int i = 1; i < this.staticSqlParts.length; i++) {
buf.append("?").append(StringUtils.toString(this.staticSqlParts[i], this.encoding));
}
return buf.toString();
}
/**
* Returns a preparable query for the specified batch count.
*
* @param count
* number of parameter batches
* @return a preparable query string with the appropriate number of placeholders
*/
public String getSqlForBatch(int count) {
QueryInfo batchInfo = getQueryInfoForBatch(count);
return batchInfo.getSqlForBatch();
}
/**
* Finds and returns the position of the first non-whitespace character from the specified SQL, skipping comments and quoted text.
*
* @param sql
* the query to search
* @param noBackslashEscapes
* whether backslash escapes are disabled or not
* @return the position of the first character of the query
*/
public static int indexOfStatementKeyword(String sql, boolean noBackslashEscapes) {
return StringUtils.indexOfNextAlphanumericChar(0, sql, OPENING_MARKERS, CLOSING_MARKERS, OVERRIDING_MARKERS,
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__BSE_MRK_COM_MYM_HNT_WS);
}
/**
* Finds and returns the first non-whitespace character from the specified SQL, skipping comments and quoted text.
*
* @param sql
* the query to search
* @param noBackslashEscapes
* whether backslash escapes are disabled or not
* @return the first character of the query, in upper case
*/
public static char firstCharOfStatementUc(String sql, boolean noBackslashEscapes) {
int statementKeywordPos = indexOfStatementKeyword(sql, noBackslashEscapes);
if (statementKeywordPos == -1) {
return Character.MIN_VALUE;
}
return Character.toUpperCase(sql.charAt(statementKeywordPos));
}
/**
* Checks whether the given query is safe to run in a read-only session. In case of doubt it is assumed to be safe. This operation does not take into
* consideration the multiplicity of queries in the specified SQL.
*
* @param sql
* the query to check
* @param noBackslashEscapes
* whether backslash escapes are disabled or not
* @return true
if the query is read-only safe, false
otherwise.
*/
public static boolean isReadOnlySafeQuery(String sql, boolean noBackslashEscapes) {
/*
* Read-only unsafe statements:
* - ALTER; CHANGE; CREATE; DELETE; DROP; GRANT; IMPORT; INSERT; INSTALL; LOAD; OPTIMIZE; RENAME; REPAIR; REPLACE; RESET; REVOKE; TRUNCATE; UNINSTALL;
* - UPDATE; WITH ... DELETE|UPDATE
*
* Read-only safe statements:
* - ANALYZE; BEGIN; BINLOG; CACHE; CALL; CHECK; CHECKSUM; CLONE; COMMIT; DEALLOCATE; DESC; DESCRIBE; EXECUTE; EXPLAIN; FLUSH; GET; HANDLER; HELP; KILL;
* - LOCK; PREPARE; PURGE; RELEASE; RESIGNAL; ROLLBACK; SAVEPOINT; SELECT; SET; SHOW; SIGNAL; START; STOP; TABLE; UNLOCK; USE; VALUES;
* - WITH ... [SELECT|TABLE|VALUES]; XA
*/
int statementKeywordPos = indexOfStatementKeyword(sql, noBackslashEscapes);
if (statementKeywordPos == -1) {
return true; // Assume it's safe.
}
char firstStatementChar = Character.toUpperCase(sql.charAt(statementKeywordPos));
if (firstStatementChar == 'A' && StringUtils.startsWithIgnoreCaseAndWs(sql, "ALTER", statementKeywordPos)) {
return false;
} else if (firstStatementChar == 'C' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "CHANGE", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "CREATE", statementKeywordPos))) {
return false;
} else if (firstStatementChar == 'D' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "DELETE", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "DROP", statementKeywordPos))) {
return false;
} else if (firstStatementChar == 'G' && StringUtils.startsWithIgnoreCaseAndWs(sql, "GRANT", statementKeywordPos)) {
return false;
} else if (firstStatementChar == 'I' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "IMPORT", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "INSTALL", statementKeywordPos))) {
return false;
} else if (firstStatementChar == 'L' && StringUtils.startsWithIgnoreCaseAndWs(sql, "LOAD", statementKeywordPos)) {
return false;
} else if (firstStatementChar == 'O' && StringUtils.startsWithIgnoreCaseAndWs(sql, "OPTIMIZE", statementKeywordPos)) {
return false;
} else if (firstStatementChar == 'R' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "RENAME", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "REPAIR", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "REPLACE", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "RESET", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "REVOKE", statementKeywordPos))) {
return false;
} else if (firstStatementChar == 'T' && StringUtils.startsWithIgnoreCaseAndWs(sql, "TRUNCATE", statementKeywordPos)) {
return false;
} else if (firstStatementChar == 'U' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "UNINSTALL", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "UPDATE", statementKeywordPos))) {
return false;
} else if (firstStatementChar == 'W' && StringUtils.startsWithIgnoreCaseAndWs(sql, "WITH", statementKeywordPos)) {
String context = getContextForWithStatement(sql, noBackslashEscapes);
return context == null || !context.equalsIgnoreCase("DELETE") && !context.equalsIgnoreCase("UPDATE");
}
return true; // Assume it's safe by default.
}
/**
* Returns the type of return that can be expected from executing the given query. This operation does not take into consideration the multiplicity
* of queries in the specified SQL.
*
* @param sql
* the query to check
* @param noBackslashEscapes
* whether backslash escapes are disabled or not
* @return the return type that can be expected from the given query, one of the elements of {@link QueryReturnType}.
*/
public static QueryReturnType getQueryReturnType(String sql, boolean noBackslashEscapes) {
/*
* Statements that return results:
* - ANALYZE; CHECK/CHECKSUM; DESC/DESCRIBE; EXPLAIN; HELP; OPTIMIZE; REPAIR; SELECT; SHOW; TABLE; VALUES; WITH ... SELECT|TABLE|VALUES ...; XA RECOVER;
*
* Statements that may return results:
* - CALL; EXECUTE;
*
* Statements that do not return results:
* - ALTER; BINLOG; CACHE; CHANGE; CLONE; COMMIT; CREATE; DEALLOCATE; DELETE; DO; DROP; FLUSH; GET; GRANT; HANDLER; IMPORT; INSERT; INSTALL; KILL; LOAD;
* - LOCK; PREPARE; PURGE; RELEASE; RENAME; REPLACE; RESET; RESIGNAL; RESTART; REVOKE; ROLLBACK; SAVEPOINT; SET; SHUTDOWN; SIGNAL; START; STOP;
* - TRUNCATE; UNINSTALL; UNLOCK; UPDATE; USE; WITH ... DELETE|UPDATE ...; XA [!RECOVER];
*/
int statementKeywordPos = indexOfStatementKeyword(sql, noBackslashEscapes);
if (statementKeywordPos == -1) {
return QueryReturnType.NONE;
}
char firstStatementChar = Character.toUpperCase(sql.charAt(statementKeywordPos));
if (firstStatementChar == 'A' && StringUtils.startsWithIgnoreCaseAndWs(sql, "ANALYZE", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'C' && StringUtils.startsWithIgnoreCaseAndWs(sql, "CALL", statementKeywordPos)) {
return QueryReturnType.MAY_PRODUCE_RESULT_SET;
} else if (firstStatementChar == 'C' && StringUtils.startsWithIgnoreCaseAndWs(sql, "CHECK", statementKeywordPos)) { // Also matches "CHECKSUM".
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'D' && StringUtils.startsWithIgnoreCaseAndWs(sql, "DESC", statementKeywordPos)) { // Also matches "DESCRIBE".
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'E' && StringUtils.startsWithIgnoreCaseAndWs(sql, "EXPLAIN", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'E' && StringUtils.startsWithIgnoreCaseAndWs(sql, "EXECUTE", statementKeywordPos)) {
return QueryReturnType.MAY_PRODUCE_RESULT_SET;
} else if (firstStatementChar == 'H' && StringUtils.startsWithIgnoreCaseAndWs(sql, "HELP", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'O' && StringUtils.startsWithIgnoreCaseAndWs(sql, "OPTIMIZE", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'R' && StringUtils.startsWithIgnoreCaseAndWs(sql, "REPAIR", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'S' && (StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT", statementKeywordPos)
|| StringUtils.startsWithIgnoreCaseAndWs(sql, "SHOW", statementKeywordPos))) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'T' && StringUtils.startsWithIgnoreCaseAndWs(sql, "TABLE", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'V' && StringUtils.startsWithIgnoreCaseAndWs(sql, "VALUES", statementKeywordPos)) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else if (firstStatementChar == 'W' && StringUtils.startsWithIgnoreCaseAndWs(sql, "WITH", statementKeywordPos)) {
String context = getContextForWithStatement(sql, noBackslashEscapes);
if (context == null) {
return QueryReturnType.MAY_PRODUCE_RESULT_SET;
} else if (context.equalsIgnoreCase("SELECT") || context.equalsIgnoreCase("TABLE") || context.equalsIgnoreCase("VALUES")) {
return QueryReturnType.PRODUCES_RESULT_SET;
} else {
return QueryReturnType.DOES_NOT_PRODUCE_RESULT_SET;
}
} else if (firstStatementChar == 'X' && StringUtils.indexOfIgnoreCase(statementKeywordPos, sql, new String[] { "XA", "RECOVER" }, OPENING_MARKERS,
CLOSING_MARKERS, noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__FULL) == statementKeywordPos) {
return QueryReturnType.PRODUCES_RESULT_SET;
}
return QueryReturnType.DOES_NOT_PRODUCE_RESULT_SET;
}
/**
* Returns the context of the WITH statement. The context can be: SELECT, TABLE, VALUES, UPDATE or DELETE. This operation does not take into consideration
* the multiplicity of queries in the specified SQL.
*
* @param sql
* the query to search
* @param noBackslashEscapes
* whether backslash escapes are disabled or not
* @return the context of the WITH statement or null if failed to find it
*/
private static String getContextForWithStatement(String sql, boolean noBackslashEscapes) {
// Must remove all comments first.
String commentsFreeSql = StringUtils.stripCommentsAndHints(sql, OPENING_MARKERS, CLOSING_MARKERS, !noBackslashEscapes);
// Iterate through statement words, skipping all sub-queries sections enclosed by parens.
StringInspector strInspector = new StringInspector(commentsFreeSql, OPENING_MARKERS + "(", CLOSING_MARKERS + ")", OPENING_MARKERS,
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__BSE_MRK_COM_MYM_HNT_WS);
boolean asFound = false;
while (true) {
int nws = strInspector.indexOfNextNonWsChar();
if (nws == -1) { // No more parts to analyze.
return null;
}
int ws = strInspector.indexOfNextWsChar();
if (ws == -1) { // End of query.
ws = commentsFreeSql.length();
}
String section = commentsFreeSql.substring(nws, ws);
if (!asFound && section.equalsIgnoreCase(AS_CLAUSE)) {
asFound = true; // Since the subquery part is skipped, this must be followed by a "," or the context statement.
} else if (asFound) {
if (section.equalsIgnoreCase(",")) {
asFound = false; // Another CTE is expected.
} else {
return section;
}
}
}
}
/**
* Checks whether the specified SQL contains or not an ON DUPLICATE KEY UPDATE clause. This operation does not take into consideration the multiplicity of
* queries in the specified SQL.
*
* @param sql
* the query to search
* @param noBackslashEscapes
* whether backslash escapes are disabled or not.
* @return true
if the query contains an ON DUPLICATE KEY UPDATE clause, false
otherwise
*/
public static boolean containsOnDuplicateKeyUpdateClause(String sql, boolean noBackslashEscapes) {
return StringUtils.indexOfIgnoreCase(0, sql, ODKU_CLAUSE, OPENING_MARKERS, CLOSING_MARKERS,
noBackslashEscapes ? SearchMode.__MRK_COM_MYM_HNT_WS : SearchMode.__BSE_MRK_COM_MYM_HNT_WS) != -1;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy