Skip to content
Permalink
Browse files

feat: Added to SQL parser to allow inspection of Statement attributes…

… and Command.

use binary trick for tolower, optimize switch, added tests
  • Loading branch information
whitingjr authored and davecramer committed Apr 15, 2016
1 parent e591577 commit 4ddb693ff073c128116b0d79f5aeb45c5ae7307c
@@ -0,0 +1,64 @@
/*-------------------------------------------------------------------------
*
* Copyright (c) 2003-2016, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/

package org.postgresql.core;

import static org.postgresql.core.DMLCommandType.INSERT;

/**
* Data Modification Language inspection support.
*
* @author Jeremy Whiting jwhiting@redhat.com
*
*/
public class DMLCommand {

public boolean isBatchedReWriteCompatible() {
return batchedReWriteCompatible;
}

public DMLCommandType getType() {
return commandType;
}

public boolean isReturningKeywordPresent() {
return parsedSQLhasRETURNINGKeyword;
}

public static DMLCommand createStatementTypeInfo(DMLCommandType type,
boolean isBatchedReWritePropertyConfigured,
boolean isBatchedReWriteCompatible, boolean isRETURNINGkeywordPresent,
boolean autocommit, int priorQueryCount) {
return new DMLCommand(type, isBatchedReWritePropertyConfigured,
isBatchedReWriteCompatible, isRETURNINGkeywordPresent, autocommit,
priorQueryCount);
}

public static DMLCommand createStatementTypeInfo(DMLCommandType type) {
return new DMLCommand(type, false, false, false, false,0);
}

public static DMLCommand createStatementTypeInfo(DMLCommandType type,
boolean isRETURNINGkeywordPresent) {
return new DMLCommand(type, false, false, isRETURNINGkeywordPresent, false,0);
}

private DMLCommand(DMLCommandType type, boolean isBatchedReWriteConfigured,
boolean isCompatible, boolean isPresent, boolean isautocommitConfigured,
int priorQueryCount) {
commandType = type;
parsedSQLhasRETURNINGKeyword = isPresent;
batchedReWriteCompatible = (type == INSERT) && isBatchedReWriteConfigured
&& isCompatible && !isautocommitConfigured
&& !isPresent && priorQueryCount == 0;
}

private final DMLCommandType commandType;
private final boolean parsedSQLhasRETURNINGKeyword;
private final boolean batchedReWriteCompatible;
}
@@ -0,0 +1,42 @@
/*-------------------------------------------------------------------------
*
* Copyright (c) 2003-2016, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/

package org.postgresql.core;

/**
* Type information inspection support.
* @author Jeremy Whiting jwhiting@redhat.com
*
*/

public enum DMLCommandType {

INSERT(true),
/**
* Use BLANK for empty sql queries or when parsing the sql string is not
* necessary.
*/
BLANK(false),
MOVE(false),
UPDATE(false),
DELETE(false);

/* to be added when needed SELECT(false), DELETE(false), UPDATE(false),
* COMMIT(false), ROLLBACK(false); */

public boolean canSupportBatchedReWrite() {
return canSupportBatchedReWrite;
}

private final boolean canSupportBatchedReWrite;

private DMLCommandType(boolean reWriteSupport) {
canSupportBatchedReWrite = reWriteSupport;
}

}
@@ -0,0 +1,34 @@
/*-------------------------------------------------------------------------
*
* Copyright (c) 2003-2016, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/

package org.postgresql.core;

/**
* Reserved SQL keywords.
* @author Jeremy Whiting jwhiting@redhat.com
*
*/
public enum Keyword {
RETURNING("RETURNING"), INTO("INTO"), VALUES("VALUES"), GROUP_BY("GROUP BY");
/* add when needed: FROM, WHERE, HAVING, ONLY, ORDER, JOIN, INNER
, LEFT, RIGHT, OUTER, LIMIT, OFFSET;*/

Keyword(String upperCaseWord) {
keyword = upperCaseWord;
}

public String asLowerCase() {
return keyword.toLowerCase();
}

public String toString() {
return keyword;
}

private final String keyword;
}
@@ -19,23 +19,23 @@

public final String nativeSql;
public final int[] bindPositions;
public final boolean isBatchedReWriteCompatible;
public final DMLCommand command;

static {
for (int i = 1; i < BIND_NAMES.length; i++) {
BIND_NAMES[i] = "$" + i;
}
}

public NativeQuery(String nativeSql) {
this(nativeSql, NO_BINDS, false);
public NativeQuery(String nativeSql, DMLCommand dml) {
this(nativeSql, NO_BINDS, dml);
}

public NativeQuery(String nativeSql, int[] bindPositions, boolean insertReWriteCompatible) {
public NativeQuery(String nativeSql, int[] bindPositions, DMLCommand dml) {
this.nativeSql = nativeSql;
this.bindPositions =
bindPositions == null || bindPositions.length == 0 ? NO_BINDS : bindPositions;
this.isBatchedReWriteCompatible = insertReWriteCompatible;
this.command = dml;
}

/**
@@ -82,4 +82,8 @@ public static String bindName(int index) {
public static int bindCount() {
return BIND_NAMES.length;
}

public DMLCommand getCommand() {
return command;
}
}
@@ -34,12 +34,16 @@
* in single quote literals
* @param withParameters whether to replace ?, ? with $1, $2, etc
* @param splitStatements whether to split statements by semicolon
* @param isAutoCommit whether autocommit is enabled
* @param isBatchedReWriteConfigured whether re-write optimization is enabled
* @return list of native queries
*/
public static List<NativeQuery> parseJdbcSql(String query, boolean standardConformingStrings,
boolean withParameters, boolean splitStatements, boolean isAutoCommit) {
boolean withParameters, boolean splitStatements, boolean isAutoCommit,
boolean isBatchedReWriteConfigured) {
if (!withParameters && !splitStatements) {
return Collections.singletonList(new NativeQuery(query));
return Collections.singletonList(new NativeQuery(query,
DMLCommand.createStatementTypeInfo(DMLCommandType.BLANK)));
}

int fragmentStart = 0;
@@ -52,14 +56,16 @@
List<NativeQuery> nativeQueries = null;
boolean isCurrentReWriteCompatible = false;
int afterValuesParens = 0;
boolean insertFound = false;
boolean isInsertPresent = false;
boolean isReturningPresent = false;
DMLCommandType current = DMLCommandType.BLANK;

boolean whitespaceOnly = true;
for (int i = 0; i < aChars.length; ++i) {
char aChar = aChars[i];
// ';' is ignored as it splits the queries
whitespaceOnly &= aChar == ';' || Character.isWhitespace(aChar);
switch (aChar) {
switch (Character.toUpperCase(aChar)) {
case '\'': // single-quotes
i = Parser.parseSingleQuotes(aChars, i, standardConformingStrings);
break;
@@ -124,53 +130,59 @@
nativeQueries = new ArrayList<NativeQuery>();
}

nativeQueries.add(new NativeQuery(nativeSql.toString(), toIntArray(bindPositions),isCurrentReWriteCompatible));
nativeQueries.add(new NativeQuery(nativeSql.toString(),
toIntArray(bindPositions), DMLCommand.createStatementTypeInfo(
current, isBatchedReWriteConfigured, isCurrentReWriteCompatible,
isReturningPresent, isAutoCommit, nativeQueries.size())));
}
// Prepare for next query
if (bindPositions != null) {
bindPositions.clear();
}
nativeSql.setLength(0);
current = DMLCommandType.BLANK;
isReturningPresent = false;
}
break;

case 'i':
if (Parser.parseInsertKeyword(aChars, i)) {
if ( !insertFound && (nativeQueries == null ? true : nativeQueries.size() == 0)) {
isCurrentReWriteCompatible = true;
insertFound = true;
} else {
isCurrentReWriteCompatible = false;
}
case 'D':
if (Parser.parseDeleteKeyword(aChars, i)) {
current = DMLCommandType.DELETE;
i += 5;
}
break;

case 'I':
if (Parser.parseInsertKeyword(aChars, i)) {
if ( !insertFound && (nativeQueries == null ? true : nativeQueries.size() == 0)) {
if ( !isInsertPresent && (nativeQueries == null ? true : nativeQueries.size() == 0)) {
isCurrentReWriteCompatible = true;
insertFound = true;
isInsertPresent = true;
current = DMLCommandType.INSERT;
} else {
isCurrentReWriteCompatible = false;
}
i += 5;
}
break;

case 'r':
// exclude re-write of insert statements with returning keyword
isCurrentReWriteCompatible = isCurrentReWriteCompatible && (Parser.parseReturningKeyword(aChars, i) == false);
case 'M':
if (Parser.parseMoveKeyword(aChars, i)) {
current = DMLCommandType.MOVE;
i += 3;
}
break;

case 'R':
// exclude re-write of insert statements with RETURNING keyword
isCurrentReWriteCompatible = isCurrentReWriteCompatible && (Parser.parseReturningKeyword(aChars, i) == false);
isReturningPresent = Parser.parseReturningKeyword(aChars, i);
if (isReturningPresent) {
i += 8;
}
break;

case 'v':
if (Parser.parseValuesKeyword(aChars, i)) {
afterValuesParens = 0 ;
case 'U':
if (Parser.parseUpdateKeyword(aChars, i)) {
current = DMLCommandType.UPDATE;
i += 5;
}
break;
@@ -186,8 +198,6 @@
break;
}
}
isCurrentReWriteCompatible = isCurrentReWriteCompatible && !isAutoCommit
&& (nativeQueries == null ? true : nativeQueries.size() == 0);

if (fragmentStart < aChars.length && !whitespaceOnly) {
nativeSql.append(aChars, fragmentStart, aChars.length - fragmentStart);
@@ -197,7 +207,11 @@
return nativeQueries != null ? nativeQueries : Collections.<NativeQuery>emptyList();
}

NativeQuery lastQuery = new NativeQuery(nativeSql.toString(), toIntArray(bindPositions), isCurrentReWriteCompatible);
NativeQuery lastQuery = new NativeQuery(nativeSql.toString(),
toIntArray(bindPositions), DMLCommand.createStatementTypeInfo(current,
isBatchedReWriteConfigured, isCurrentReWriteCompatible,
isReturningPresent, isAutoCommit, (nativeQueries == null ? 0 :
nativeQueries.size())));

if (nativeQueries == null) {
return Collections.singletonList(lastQuery);
@@ -395,6 +409,26 @@ public static int parseBlockComment(final char[] query, int offset) {
return offset;
}

/**
* Parse string to check presence of DELETE keyword regardless of case.
* The initial character is assumed to have been matched.
* @param query char[] of the query statement
* @param offset position of query to start checking
* @return boolean indicates presence of word
*/
public static boolean parseDeleteKeyword(final char[] query, int offset) {
if (query.length < (offset + 6)) {
return false;
}

return Character.toUpperCase(query[offset + 1]) == 'D'
&& Character.toUpperCase(query[offset + 1]) == 'E'
&& Character.toUpperCase(query[offset + 2]) == 'L'
&& Character.toUpperCase(query[offset + 3]) == 'E'
&& Character.toUpperCase(query[offset + 4]) == 'T'
&& Character.toUpperCase(query[offset + 5]) == 'E';
}

/**
* Parse string to check presence of INSERT keyword regardless of case.
* @param query char[] of the query statement
@@ -414,6 +448,23 @@ public static boolean parseInsertKeyword(final char[] query, int offset) {
&& Character.toUpperCase(query[offset + 5]) == 'T';
}

/**
* Parse string to check presence of MOVE keyword regardless of case.
* @param query char[] of the query statement
* @param offset position of query to start checking
* @return boolean indicates presence of word
*/
public static boolean parseMoveKeyword(final char[] query, int offset) {
if (query.length < (offset + 4)) {
return false;
}

return Character.toUpperCase(query[offset + 1]) == 'M'
&& Character.toUpperCase(query[offset + 1]) == 'O'
&& Character.toUpperCase(query[offset + 2]) == 'V'
&& Character.toUpperCase(query[offset + 3]) == 'E';
}

/**
* Parse string to check presence of RETURNING keyword regardless of case.
* @param query char[] of the query statement
@@ -436,6 +487,25 @@ public static boolean parseReturningKeyword(final char[] query, int offset) {
&& Character.toUpperCase(query[offset + 8]) == 'G';
}

/**
* Parse string to check presence of UPDATE keyword regardless of case.
* @param query char[] of the query statement
* @param offset position of query to start checking
* @return boolean indicates presence of word
*/
public static boolean parseUpdateKeyword(final char[] query, int offset) {
if (query.length < (offset + 6)) {
return false;
}

return Character.toUpperCase(query[offset + 1]) == 'U'
&& Character.toUpperCase(query[offset + 1]) == 'P'
&& Character.toUpperCase(query[offset + 2]) == 'D'
&& Character.toUpperCase(query[offset + 3]) == 'A'
&& Character.toUpperCase(query[offset + 4]) == 'T'
&& Character.toUpperCase(query[offset + 5]) == 'E';
}

/**
* Parse string to check presence of VALUES keyword regardless of case.
* @param query char[] of the query statement

1 comment on commit 4ddb693

@davecramer

This comment has been minimized.

Copy link
Member

@davecramer davecramer commented on 4ddb693 Jun 13, 2016

Looks like this has been fixed already

Please sign in to comment.
You can’t perform that action at this time.