Skip to content

Commit

Permalink
fix: Parser does not handle version 14 function syntax Issue pgjdbc#2507
Browse files Browse the repository at this point in the history
 (pgjdbc#2703)

* fix: Parser does not handle version 14 function syntax Issue pgjdbc#2507

* Change the logic to look for BEGIN ATOMIC instead of just ATOMIC

* document parser, refactor Brace to Parenthesis
  • Loading branch information
davecramer authored Dec 29, 2022
1 parent 06f8175 commit e7239c1
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 29 deletions.
121 changes: 92 additions & 29 deletions pgjdbc/src/main/java/org/postgresql/core/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
List<NativeQuery> nativeQueries = null;
boolean isCurrentReWriteCompatible = false;
boolean isValuesFound = false;
int valuesBraceOpenPosition = -1;
int valuesBraceClosePosition = -1;
boolean valuesBraceCloseFound = false;
int valuesParenthesisOpenPosition = -1;
int valuesParenthesisClosePosition = -1;
boolean valuesParenthesisCloseFound = false;
boolean isInsertPresent = false;
boolean isReturningPresent = false;
boolean isReturningPresentPrev = false;
boolean isBeginPresent = false;
boolean isBeginAtomicPresent = false;
SqlCommandType currentCommandType = SqlCommandType.BLANK;
SqlCommandType prevCommandType = SqlCommandType.BLANK;
int numberOfStatements = 0;
Expand All @@ -80,10 +82,15 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
int keyWordCount = 0;
int keywordStart = -1;
int keywordEnd = -1;
/*
loop through looking for keywords, single quotes, double quotes, comments, dollar quotes,
parenthesis, ? and ;
for single/double/dollar quotes, and comments we just want to move the index
*/
for (int i = 0; i < aChars.length; ++i) {
char aChar = aChars[i];
boolean isKeyWordChar = false;
// ';' is ignored as it splits the queries
// ';' is ignored as it splits the queries. We do have to deal with ; in BEGIN ATOMIC functions
whitespaceOnly &= aChar == ';' || Character.isWhitespace(aChar);
keywordEnd = i; // parseSingleQuotes, parseDoubleQuotes, etc move index so we keep old value
switch (aChar) {
Expand Down Expand Up @@ -111,10 +118,10 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo

case ')':
inParen--;
if (inParen == 0 && isValuesFound && !valuesBraceCloseFound) {
if (inParen == 0 && isValuesFound && !valuesParenthesisCloseFound) {
// If original statement is multi-values like VALUES (...), (...), ... then
// search for the latest closing paren
valuesBraceClosePosition = nativeSql.length() + i - fragmentStart;
valuesParenthesisClosePosition = nativeSql.length() + i - fragmentStart;
}
break;

Expand All @@ -139,7 +146,8 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
break;

case ';':
if (inParen == 0) {
// we don't split the queries if BEGIN ATOMIC is present
if (!isBeginAtomicPresent && inParen == 0) {
if (!whitespaceOnly) {
numberOfStatements++;
nativeSql.append(aChars, fragmentStart, i - fragmentStart);
Expand All @@ -156,18 +164,18 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
nativeQueries = new ArrayList<NativeQuery>();
}

if (!isValuesFound || !isCurrentReWriteCompatible || valuesBraceClosePosition == -1
if (!isValuesFound || !isCurrentReWriteCompatible || valuesParenthesisClosePosition == -1
|| (bindPositions != null
&& valuesBraceClosePosition < bindPositions.get(bindPositions.size() - 1))) {
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
&& valuesParenthesisClosePosition < bindPositions.get(bindPositions.size() - 1))) {
valuesParenthesisOpenPosition = -1;
valuesParenthesisClosePosition = -1;
}

nativeQueries.add(new NativeQuery(nativeSql.toString(),
toIntArray(bindPositions), false,
SqlCommand.createStatementTypeInfo(
currentCommandType, isBatchedReWriteConfigured, valuesBraceOpenPosition,
valuesBraceClosePosition,
currentCommandType, isBatchedReWriteConfigured, valuesParenthesisOpenPosition,
valuesParenthesisClosePosition,
isReturningPresent, nativeQueries.size())));
}
}
Expand All @@ -183,9 +191,9 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
nativeSql.setLength(0);
isValuesFound = false;
isCurrentReWriteCompatible = false;
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
valuesBraceCloseFound = false;
valuesParenthesisOpenPosition = -1;
valuesParenthesisClosePosition = -1;
valuesParenthesisCloseFound = false;
}
}
break;
Expand All @@ -202,10 +210,10 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
isKeyWordChar = isIdentifierStartChar(aChar);
if (isKeyWordChar) {
keywordStart = i;
if (valuesBraceOpenPosition != -1 && inParen == 0) {
if (valuesParenthesisOpenPosition != -1 && inParen == 0) {
// When the statement already has multi-values, stop looking for more of them
// Since values(?,?),(?,?),... should not contain keywords in the middle
valuesBraceCloseFound = true;
valuesParenthesisCloseFound = true;
}
}
break;
Expand Down Expand Up @@ -238,15 +246,32 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
isCurrentReWriteCompatible = false;
}
}

} else if (currentCommandType == SqlCommandType.WITH
&& inParen == 0) {
SqlCommandType command = parseWithCommandType(aChars, i, keywordStart, wordLength);
if (command != null) {
currentCommandType = command;
}
} else if (currentCommandType == SqlCommandType.CREATE) {
/*
We are looking for BEGIN ATOMIC
*/
if (wordLength == 5 && parseBeginKeyword(aChars, keywordStart)) {
isBeginPresent = true;
} else {
// found begin, now look for atomic
if (isBeginPresent == true) {
if (wordLength == 6 && parseAtomicKeyword(aChars, keywordStart)) {
isBeginAtomicPresent = true;
}
// either way we reset beginFound
isBeginPresent = false;
}
}
}
if (inParen != 0 || aChar == ')') {
// RETURNING and VALUES cannot be present in braces
// RETURNING and VALUES cannot be present in parentheses
} else if (wordLength == 9 && parseReturningKeyword(aChars, keywordStart)) {
isReturningPresent = true;
} else if (wordLength == 6 && parseValuesKeyword(aChars, keywordStart)) {
Expand All @@ -257,17 +282,17 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
}
if (aChar == '(') {
inParen++;
if (inParen == 1 && isValuesFound && valuesBraceOpenPosition == -1) {
valuesBraceOpenPosition = nativeSql.length() + i - fragmentStart;
if (inParen == 1 && isValuesFound && valuesParenthesisOpenPosition == -1) {
valuesParenthesisOpenPosition = nativeSql.length() + i - fragmentStart;
}
}
}

if (!isValuesFound || !isCurrentReWriteCompatible || valuesBraceClosePosition == -1
if (!isValuesFound || !isCurrentReWriteCompatible || valuesParenthesisClosePosition == -1
|| (bindPositions != null
&& valuesBraceClosePosition < bindPositions.get(bindPositions.size() - 1))) {
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
&& valuesParenthesisClosePosition < bindPositions.get(bindPositions.size() - 1))) {
valuesParenthesisOpenPosition = -1;
valuesParenthesisClosePosition = -1;
}

if (fragmentStart < aChars.length && !whitespaceOnly) {
Expand All @@ -293,7 +318,7 @@ public static List<NativeQuery> parseJdbcSql(String query, boolean standardConfo
NativeQuery lastQuery = new NativeQuery(nativeSql.toString(),
toIntArray(bindPositions), !splitStatements,
SqlCommand.createStatementTypeInfo(currentCommandType,
isBatchedReWriteConfigured, valuesBraceOpenPosition, valuesBraceClosePosition,
isBatchedReWriteConfigured, valuesParenthesisOpenPosition, valuesParenthesisClosePosition,
isReturningPresent, (nativeQueries == null ? 0 : nativeQueries.size())));

if (nativeQueries == null) {
Expand Down Expand Up @@ -608,6 +633,44 @@ public static boolean parseInsertKeyword(final char[] query, int offset) {
&& (query[offset + 5] | 32) == 't';
}

/**
Parse string to check presence of BEGIN 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 parseBeginKeyword(final char[] query, int offset) {
if (query.length < (offset + 6)) {
return false;
}
return (query[offset] | 32) == 'b'
&& (query[offset + 1] | 32) == 'e'
&& (query[offset + 2] | 32) == 'g'
&& (query[offset + 3] | 32) == 'i'
&& (query[offset + 4] | 32) == 'n';
}

/**
Parse string to check presence of ATOMIC 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 parseAtomicKeyword(final char[] query, int offset) {
if (query.length < (offset + 7)) {
return false;
}
return (query[offset] | 32) == 'a'
&& (query[offset + 1] | 32) == 't'
&& (query[offset + 2] | 32) == 'o'
&& (query[offset + 3] | 32) == 'm'
&& (query[offset + 4] | 32) == 'i'
&& (query[offset + 5] | 32) == 'c';
}

/**
* Parse string to check presence of MOVE keyword regardless of case.
*
Expand Down Expand Up @@ -886,7 +949,7 @@ public static boolean isIdentifierStartChar(char c) {
* pgsql/src/backend/parser/scan.l:
* ident_start [A-Za-z\200-\377_]
* ident_cont [A-Za-z\200-\377_0-9\$]
* however is is not clear how that interacts with unicode, so we just use Java's implementation.
* however it is not clear how that interacts with unicode, so we just use Java's implementation.
*/
return Character.isJavaIdentifierStart(c);
}
Expand Down Expand Up @@ -1362,7 +1425,7 @@ private static int parseSql(char[] sql, int i, StringBuilder newsql, boolean sto
return i;
}

private static int findOpenBrace(char[] sql, int i) {
private static int findOpenParenthesis(char[] sql, int i) {
int posArgs = i;
while (posArgs < sql.length && sql[posArgs] != '(') {
posArgs++;
Expand All @@ -1383,7 +1446,7 @@ private static void checkParsePosition(int i, int len, int i0, char[] sql,

private static int escapeFunction(char[] sql, int i, StringBuilder newsql, boolean stdStrings) throws SQLException {
String functionName;
int argPos = findOpenBrace(sql, i);
int argPos = findOpenParenthesis(sql, i);
if (argPos < sql.length) {
functionName = new String(sql, i, argPos - i).trim();
// extract arguments
Expand Down
14 changes: 14 additions & 0 deletions pgjdbc/src/test/java/org/postgresql/core/ParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,18 @@ public void alterTableParseWithOnUpdateClause() throws SQLException {
Assert.assertFalse("No returning keyword should be present", command.isReturningKeywordPresent());
Assert.assertEquals(SqlCommandType.ALTER, command.getType());
}

@Test
public void testParseV14functions() throws SQLException {
String[] returningColumns = {"*"};
String query = "CREATE OR REPLACE FUNCTION asterisks(n int)\n"
+ " RETURNS SETOF text\n"
+ " LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE\n"
+ "BEGIN ATOMIC\n"
+ "SELECT repeat('*', g) FROM generate_series (1, n) g; \n"
+ "END;";
List<NativeQuery> qry = Parser.parseJdbcSql(query, true, true, true, true, true, returningColumns);
Assert.assertNotNull(qry);
Assert.assertEquals("There should only be one query returned here", 1, qry.size());
}
}

0 comments on commit e7239c1

Please sign in to comment.