/*
* Copyright (c) 2006, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.core;
import org.postgresql.jdbc.
EscapedFunctions2;
import org.postgresql.util.
GT;
import org.postgresql.util.
PSQLException;
import org.postgresql.util.
PSQLState;
import java.lang.reflect.
InvocationTargetException;
import java.lang.reflect.
Method;
import java.sql.
SQLException;
import java.util.
ArrayList;
import java.util.
Collections;
import java.util.
List;
/**
* Basic query parser infrastructure.
* Note: This class should not be considered as pgjdbc public API.
*
* @author Michael Paesold (mpaesold@gmx.at)
* @author Christopher Deckers (chrriis@gmail.com)
*/
public class
Parser {
private static final int[]
NO_BINDS = new int[0];
/**
* Parses JDBC query into PostgreSQL's native format. Several queries might be given if separated
* by semicolon.
*
* @param query jdbc query to parse
* @param standardConformingStrings whether to allow backslashes to be used as escape characters
* in single quote literals
* @param withParameters whether to replace ?, ? with $1, $2, etc
* @param splitStatements whether to split statements by semicolon
* @param isBatchedReWriteConfigured whether re-write optimization is enabled
* @param returningColumnNames for simple insert, update, delete add returning with given column names
* @return list of native queries
* @throws SQLException if unable to add returning clause (invalid column names)
*/
public static
List<
NativeQuery>
parseJdbcSql(
String query, boolean
standardConformingStrings,
boolean
withParameters, boolean
splitStatements,
boolean
isBatchedReWriteConfigured,
String...
returningColumnNames) throws
SQLException {
if (!
withParameters && !
splitStatements
&&
returningColumnNames != null &&
returningColumnNames.length == 0) {
return
Collections.
singletonList(new
NativeQuery(
query,
SqlCommand.
createStatementTypeInfo(
SqlCommandType.
BLANK)));
}
int
fragmentStart = 0;
int
inParen = 0;
char[]
aChars =
query.
toCharArray();
StringBuilder nativeSql = new
StringBuilder(
query.
length() + 10);
List<
Integer>
bindPositions = null; // initialized on demand
List<
NativeQuery>
nativeQueries = null;
boolean
isCurrentReWriteCompatible = false;
boolean
isValuesFound = false;
int
valuesBraceOpenPosition = -1;
int
valuesBraceClosePosition = -1;
boolean
valuesBraceCloseFound = false;
boolean
isInsertPresent = false;
boolean
isReturningPresent = false;
boolean
isReturningPresentPrev = false;
SqlCommandType currentCommandType =
SqlCommandType.
BLANK;
SqlCommandType prevCommandType =
SqlCommandType.
BLANK;
int
numberOfStatements = 0;
boolean
whitespaceOnly = true;
int
keyWordCount = 0;
int
keywordStart = -1;
int
keywordEnd = -1;
for (int
i = 0;
i <
aChars.length; ++
i) {
char
aChar =
aChars[
i];
boolean
isKeyWordChar = false;
// ';' is ignored as it splits the queries
whitespaceOnly &=
aChar == ';' ||
Character.
isWhitespace(
aChar);
keywordEnd =
i; // parseSingleQuotes, parseDoubleQuotes, etc move index so we keep old value
switch (
aChar) {
case '\'': // single-quotes
i =
Parser.
parseSingleQuotes(
aChars,
i,
standardConformingStrings);
break;
case '"': // double-quotes
i =
Parser.
parseDoubleQuotes(
aChars,
i);
break;
case '-': // possibly -- style comment
i =
Parser.
parseLineComment(
aChars,
i);
break;
case '/': // possibly /* */ style comment
i =
Parser.
parseBlockComment(
aChars,
i);
break;
case '$': // possibly dollar quote start
i =
Parser.
parseDollarQuotes(
aChars,
i);
break;
// case '(' moved below to parse "values(" properly
case ')':
inParen--;
if (
inParen == 0 &&
isValuesFound && !
valuesBraceCloseFound) {
// If original statement is multi-values like VALUES (...), (...), ... then
// search for the latest closing paren
valuesBraceClosePosition =
nativeSql.
length() +
i -
fragmentStart;
}
break;
case '?':
nativeSql.
append(
aChars,
fragmentStart,
i -
fragmentStart);
if (
i + 1 <
aChars.length &&
aChars[
i + 1] == '?') /* replace ?? with ? */ {
nativeSql.
append('?');
i++; // make sure the coming ? is not treated as a bind
} else {
if (!
withParameters) {
nativeSql.
append('?');
} else {
if (
bindPositions == null) {
bindPositions = new
ArrayList<
Integer>();
}
bindPositions.
add(
nativeSql.
length());
int
bindIndex =
bindPositions.
size();
nativeSql.
append(
NativeQuery.
bindName(
bindIndex));
}
}
fragmentStart =
i + 1;
break;
case ';':
if (
inParen == 0) {
if (!
whitespaceOnly) {
numberOfStatements++;
nativeSql.
append(
aChars,
fragmentStart,
i -
fragmentStart);
whitespaceOnly = true;
}
fragmentStart =
i + 1;
if (
nativeSql.
length() > 0) {
if (
addReturning(
nativeSql,
currentCommandType,
returningColumnNames,
isReturningPresent)) {
isReturningPresent = true;
}
if (
splitStatements) {
if (
nativeQueries == null) {
nativeQueries = new
ArrayList<
NativeQuery>();
}
if (!
isValuesFound || !
isCurrentReWriteCompatible ||
valuesBraceClosePosition == -1
|| (
bindPositions != null
&&
valuesBraceClosePosition <
bindPositions.
get(
bindPositions.
size() - 1))) {
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
}
nativeQueries.
add(new
NativeQuery(
nativeSql.
toString(),
toIntArray(
bindPositions), false,
SqlCommand.
createStatementTypeInfo(
currentCommandType,
isBatchedReWriteConfigured,
valuesBraceOpenPosition,
valuesBraceClosePosition,
isReturningPresent,
nativeQueries.
size())));
}
}
prevCommandType =
currentCommandType;
isReturningPresentPrev =
isReturningPresent;
currentCommandType =
SqlCommandType.
BLANK;
isReturningPresent = false;
if (
splitStatements) {
// Prepare for next query
if (
bindPositions != null) {
bindPositions.
clear();
}
nativeSql.
setLength(0);
isValuesFound = false;
isCurrentReWriteCompatible = false;
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
valuesBraceCloseFound = false;
}
}
break;
default:
if (
keywordStart >= 0) {
// When we are inside a keyword, we need to detect keyword end boundary
// Note that isKeyWordChar is initialized to false before the switch, so
// all other characters would result in isKeyWordChar=false
isKeyWordChar =
isIdentifierContChar(
aChar);
break;
}
// Not in keyword, so just detect next keyword start
isKeyWordChar =
isIdentifierStartChar(
aChar);
if (
isKeyWordChar) {
keywordStart =
i;
if (
valuesBraceOpenPosition != -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;
}
}
break;
}
if (
keywordStart >= 0 && (
i ==
aChars.length - 1 || !
isKeyWordChar)) {
int
wordLength = (
isKeyWordChar ?
i + 1 :
keywordEnd) -
keywordStart;
if (
currentCommandType ==
SqlCommandType.
BLANK) {
if (
wordLength == 6 &&
parseUpdateKeyword(
aChars,
keywordStart)) {
currentCommandType =
SqlCommandType.
UPDATE;
} else if (
wordLength == 6 &&
parseDeleteKeyword(
aChars,
keywordStart)) {
currentCommandType =
SqlCommandType.
DELETE;
} else if (
wordLength == 4 &&
parseMoveKeyword(
aChars,
keywordStart)) {
currentCommandType =
SqlCommandType.
MOVE;
} else if (
wordLength == 6 &&
parseSelectKeyword(
aChars,
keywordStart)) {
currentCommandType =
SqlCommandType.
SELECT;
} else if (
wordLength == 4 &&
parseWithKeyword(
aChars,
keywordStart)) {
currentCommandType =
SqlCommandType.
WITH;
} else if (
wordLength == 6 &&
parseInsertKeyword(
aChars,
keywordStart)) {
if (!
isInsertPresent && (
nativeQueries == null ||
nativeQueries.
isEmpty())) {
// Only allow rewrite for insert command starting with the insert keyword.
// Else, too many risks of wrong interpretation.
isCurrentReWriteCompatible =
keyWordCount == 0;
isInsertPresent = true;
currentCommandType =
SqlCommandType.
INSERT;
} else {
isCurrentReWriteCompatible = false;
}
}
} else if (
currentCommandType ==
SqlCommandType.
WITH
&&
inParen == 0) {
SqlCommandType command =
parseWithCommandType(
aChars,
i,
keywordStart,
wordLength);
if (
command != null) {
currentCommandType =
command;
}
}
if (
inParen != 0 ||
aChar == ')') {
// RETURNING and VALUES cannot be present in braces
} else if (
wordLength == 9 &&
parseReturningKeyword(
aChars,
keywordStart)) {
isReturningPresent = true;
} else if (
wordLength == 6 &&
parseValuesKeyword(
aChars,
keywordStart)) {
isValuesFound = true;
}
keywordStart = -1;
keyWordCount++;
}
if (
aChar == '(') {
inParen++;
if (
inParen == 1 &&
isValuesFound &&
valuesBraceOpenPosition == -1) {
valuesBraceOpenPosition =
nativeSql.
length() +
i -
fragmentStart;
}
}
}
if (!
isValuesFound || !
isCurrentReWriteCompatible ||
valuesBraceClosePosition == -1
|| (
bindPositions != null
&&
valuesBraceClosePosition <
bindPositions.
get(
bindPositions.
size() - 1))) {
valuesBraceOpenPosition = -1;
valuesBraceClosePosition = -1;
}
if (
fragmentStart <
aChars.length && !
whitespaceOnly) {
nativeSql.
append(
aChars,
fragmentStart,
aChars.length -
fragmentStart);
} else {
if (
numberOfStatements > 1) {
isReturningPresent = false;
currentCommandType =
SqlCommandType.
BLANK;
} else if (
numberOfStatements == 1) {
isReturningPresent =
isReturningPresentPrev;
currentCommandType =
prevCommandType;
}
}
if (
nativeSql.
length() == 0) {
return
nativeQueries != null ?
nativeQueries :
Collections.<
NativeQuery>
emptyList();
}
if (
addReturning(
nativeSql,
currentCommandType,
returningColumnNames,
isReturningPresent)) {
isReturningPresent = true;
}
NativeQuery lastQuery = new
NativeQuery(
nativeSql.
toString(),
toIntArray(
bindPositions), !
splitStatements,
SqlCommand.
createStatementTypeInfo(
currentCommandType,
isBatchedReWriteConfigured,
valuesBraceOpenPosition,
valuesBraceClosePosition,
isReturningPresent, (
nativeQueries == null ? 0 :
nativeQueries.
size())));
if (
nativeQueries == null) {
return
Collections.
singletonList(
lastQuery);
}
if (!
whitespaceOnly) {
nativeQueries.
add(
lastQuery);
}
return
nativeQueries;
}
private static
SqlCommandType parseWithCommandType(char[]
aChars, int
i, int
keywordStart,
int
wordLength) {
// This parses `with x as (...) ...`
// Corner case is `with select as (insert ..) select * from select
SqlCommandType command;
if (
wordLength == 6 &&
parseUpdateKeyword(
aChars,
keywordStart)) {
command =
SqlCommandType.
UPDATE;
} else if (
wordLength == 6 &&
parseDeleteKeyword(
aChars,
keywordStart)) {
command =
SqlCommandType.
DELETE;
} else if (
wordLength == 6 &&
parseInsertKeyword(
aChars,
keywordStart)) {
command =
SqlCommandType.
INSERT;
} else if (
wordLength == 6 &&
parseSelectKeyword(
aChars,
keywordStart)) {
command =
SqlCommandType.
SELECT;
} else {
return null;
}
// update/delete/insert/select keyword detected
// Check if `AS` follows
int
nextInd =
i;
// The loop should skip whitespace and comments
for (;
nextInd <
aChars.length;
nextInd++) {
char
nextChar =
aChars[
nextInd];
if (
nextChar == '-') {
nextInd =
Parser.
parseLineComment(
aChars,
nextInd);
} else if (
nextChar == '/') {
nextInd =
Parser.
parseBlockComment(
aChars,
nextInd);
} else if (
Character.
isWhitespace(
nextChar)) {
// Skip whitespace
continue;
} else {
break;
}
}
if (
nextInd + 2 >=
aChars.length
|| (!
parseAsKeyword(
aChars,
nextInd)
||
isIdentifierContChar(
aChars[
nextInd + 2]))) {
return
command;
}
return null;
}
private static boolean
addReturning(
StringBuilder nativeSql,
SqlCommandType currentCommandType,
String[]
returningColumnNames, boolean
isReturningPresent) throws
SQLException {
if (
isReturningPresent ||
returningColumnNames.length == 0) {
return false;
}
if (
currentCommandType !=
SqlCommandType.
INSERT
&&
currentCommandType !=
SqlCommandType.
UPDATE
&&
currentCommandType !=
SqlCommandType.
DELETE
&&
currentCommandType !=
SqlCommandType.
WITH) {
return false;
}
nativeSql.
append("\nRETURNING ");
if (
returningColumnNames.length == 1 &&
returningColumnNames[0].
charAt(0) == '*') {
nativeSql.
append('*');
return true;
}
for (int
col = 0;
col <
returningColumnNames.length;
col++) {
String columnName =
returningColumnNames[
col];
if (
col > 0) {
nativeSql.
append(", ");
}
Utils.
escapeIdentifier(
nativeSql,
columnName);
}
return true;
}
/**
* Converts {@code List<Integer>} to {@code int[]}. Empty and {@code null} lists are converted to
* empty array.
*
* @param list input list
* @return output array
*/
private static int[]
toIntArray(
List<
Integer>
list) {
if (
list == null ||
list.
isEmpty()) {
return
NO_BINDS;
}
int[]
res = new int[
list.
size()];
for (int
i = 0;
i <
list.
size();
i++) {
res[
i] =
list.
get(
i); // must not be null
}
return
res;
}
/**
* <p>Find the end of the single-quoted string starting at the given offset.</p>
*
* <p>Note: for <tt>'single '' quote in string'</tt>, this method currently returns the offset of
* first <tt>'</tt> character after the initial one. The caller must call the method a second time
* for the second part of the quoted string.</p>
*
* @param query query
* @param offset start offset
* @param standardConformingStrings standard conforming strings
* @return position of the end of the single-quoted string
*/
public static int
parseSingleQuotes(final char[]
query, int
offset,
boolean
standardConformingStrings) {
// check for escape string syntax (E'')
if (
standardConformingStrings
&&
offset >= 2
&& (
query[
offset - 1] == 'e' ||
query[
offset - 1] == 'E')
&&
charTerminatesIdentifier(
query[
offset - 2])) {
standardConformingStrings = false;
}
if (
standardConformingStrings) {
// do NOT treat backslashes as escape characters
while (++
offset <
query.length) {
switch (
query[
offset]) {
case '\'':
return
offset;
default:
break;
}
}
} else {
// treat backslashes as escape characters
while (++
offset <
query.length) {
switch (
query[
offset]) {
case '\\':
++
offset;
break;
case '\'':
return
offset;
default:
break;
}
}
}
return
query.length;
}
/**
* <p>Find the end of the double-quoted string starting at the given offset.</p>
*
* <p>Note: for <tt>"double "" quote in string"</tt>, this method currently
* returns the offset of first <tt>"</tt> character after the initial one. The caller must
* call the method a second time for the second part of the quoted string.</p>
*
* @param query query
* @param offset start offset
* @return position of the end of the double-quoted string
*/
public static int
parseDoubleQuotes(final char[]
query, int
offset) {
while (++
offset <
query.length &&
query[
offset] != '"') {
;
}
return
offset;
}
/**
* Test if the dollar character (<tt>$</tt>) at the given offset starts a dollar-quoted string and
* return the offset of the ending dollar character.
*
* @param query query
* @param offset start offset
* @return offset of the ending dollar character
*/
public static int
parseDollarQuotes(final char[]
query, int
offset) {
if (
offset + 1 <
query.length
&& (
offset == 0 || !
isIdentifierContChar(
query[
offset - 1]))) {
int
endIdx = -1;
if (
query[
offset + 1] == '$') {
endIdx =
offset + 1;
} else if (
isDollarQuoteStartChar(
query[
offset + 1])) {
for (int
d =
offset + 2;
d <
query.length; ++
d) {
if (
query[
d] == '$') {
endIdx =
d;
break;
} else if (!
isDollarQuoteContChar(
query[
d])) {
break;
}
}
}
if (
endIdx > 0) {
// found; note: tag includes start and end $ character
int
tagIdx =
offset;
int
tagLen =
endIdx -
offset + 1;
offset =
endIdx; // loop continues at endIdx + 1
for (++
offset;
offset <
query.length; ++
offset) {
if (
query[
offset] == '$'
&&
subArraysEqual(
query,
tagIdx,
offset,
tagLen)) {
offset +=
tagLen - 1;
break;
}
}
}
}
return
offset;
}
/**
* Test if the <tt>-</tt> character at <tt>offset</tt> starts a <tt>--</tt> style line comment,
* and return the position of the first <tt>\r</tt> or <tt>\n</tt> character.
*
* @param query query
* @param offset start offset
* @return position of the first <tt>\r</tt> or <tt>\n</tt> character
*/
public static int
parseLineComment(final char[]
query, int
offset) {
if (
offset + 1 <
query.length &&
query[
offset + 1] == '-') {
while (
offset + 1 <
query.length) {
offset++;
if (
query[
offset] == '\r' ||
query[
offset] == '\n') {
break;
}
}
}
return
offset;
}
/**
* Test if the <tt>/</tt> character at <tt>offset</tt> starts a block comment, and return the
* position of the last <tt>/</tt> character.
*
* @param query query
* @param offset start offset
* @return position of the last <tt>/</tt> character
*/
public static int
parseBlockComment(final char[]
query, int
offset) {
if (
offset + 1 <
query.length &&
query[
offset + 1] == '*') {
// /* /* */ */ nest, according to SQL spec
int
level = 1;
for (
offset += 2;
offset <
query.length; ++
offset) {
switch (
query[
offset - 1]) {
case '*':
if (
query[
offset] == '/') {
--
level;
++
offset; // don't parse / in */* twice
}
break;
case '/':
if (
query[
offset] == '*') {
++
level;
++
offset; // don't parse * in /*/ twice
}
break;
default:
break;
}
if (
level == 0) {
--
offset; // reset position to last '/' char
break;
}
}
}
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 (
query[
offset] | 32) == 'd'
&& (
query[
offset + 1] | 32) == 'e'
&& (
query[
offset + 2] | 32) == 'l'
&& (
query[
offset + 3] | 32) == 'e'
&& (
query[
offset + 4] | 32) == 't'
&& (
query[
offset + 5] | 32) == 'e';
}
/**
* Parse string to check presence of INSERT 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
parseInsertKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 7)) {
return false;
}
return (
query[
offset] | 32) == 'i'
&& (
query[
offset + 1] | 32) == 'n'
&& (
query[
offset + 2] | 32) == 's'
&& (
query[
offset + 3] | 32) == 'e'
&& (
query[
offset + 4] | 32) == 'r'
&& (
query[
offset + 5] | 32) == '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 (
query[
offset] | 32) == 'm'
&& (
query[
offset + 1] | 32) == 'o'
&& (
query[
offset + 2] | 32) == 'v'
&& (
query[
offset + 3] | 32) == 'e';
}
/**
* Parse string to check presence of RETURNING 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
parseReturningKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 9)) {
return false;
}
return (
query[
offset] | 32) == 'r'
&& (
query[
offset + 1] | 32) == 'e'
&& (
query[
offset + 2] | 32) == 't'
&& (
query[
offset + 3] | 32) == 'u'
&& (
query[
offset + 4] | 32) == 'r'
&& (
query[
offset + 5] | 32) == 'n'
&& (
query[
offset + 6] | 32) == 'i'
&& (
query[
offset + 7] | 32) == 'n'
&& (
query[
offset + 8] | 32) == 'g';
}
/**
* Parse string to check presence of SELECT 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
parseSelectKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 6)) {
return false;
}
return (
query[
offset] | 32) == 's'
&& (
query[
offset + 1] | 32) == 'e'
&& (
query[
offset + 2] | 32) == 'l'
&& (
query[
offset + 3] | 32) == 'e'
&& (
query[
offset + 4] | 32) == 'c'
&& (
query[
offset + 5] | 32) == 't';
}
/**
* 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 (
query[
offset] | 32) == 'u'
&& (
query[
offset + 1] | 32) == 'p'
&& (
query[
offset + 2] | 32) == 'd'
&& (
query[
offset + 3] | 32) == 'a'
&& (
query[
offset + 4] | 32) == 't'
&& (
query[
offset + 5] | 32) == 'e';
}
/**
* Parse string to check presence of VALUES 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
parseValuesKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 6)) {
return false;
}
return (
query[
offset] | 32) == 'v'
&& (
query[
offset + 1] | 32) == 'a'
&& (
query[
offset + 2] | 32) == 'l'
&& (
query[
offset + 3] | 32) == 'u'
&& (
query[
offset + 4] | 32) == 'e'
&& (
query[
offset + 5] | 32) == 's';
}
/**
* Faster version of {@link Long#parseLong(String)} when parsing a substring is required
*
* @param s string to parse
* @param beginIndex begin index
* @param endIndex end index
* @return long value
*/
public static long
parseLong(
String s, int
beginIndex, int
endIndex) {
// Fallback to default implementation in case the string is long
if (
endIndex -
beginIndex > 16) {
return
Long.
parseLong(
s.
substring(
beginIndex,
endIndex));
}
long
res =
digitAt(
s,
beginIndex);
for (
beginIndex++;
beginIndex <
endIndex;
beginIndex++) {
res =
res * 10 +
digitAt(
s,
beginIndex);
}
return
res;
}
/**
* Parse string to check presence of WITH 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
parseWithKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 4)) {
return false;
}
return (
query[
offset] | 32) == 'w'
&& (
query[
offset + 1] | 32) == 'i'
&& (
query[
offset + 2] | 32) == 't'
&& (
query[
offset + 3] | 32) == 'h';
}
/**
* Parse string to check presence of AS 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
parseAsKeyword(final char[]
query, int
offset) {
if (
query.length < (
offset + 2)) {
return false;
}
return (
query[
offset] | 32) == 'a'
&& (
query[
offset + 1] | 32) == 's';
}
/**
* Returns true if a given string {@code s} has digit at position {@code pos}.
* @param s input string
* @param pos position (0-based)
* @return true if input string s has digit at position pos
*/
public static boolean
isDigitAt(
String s, int
pos) {
return
pos > 0 &&
pos <
s.
length() &&
Character.
isDigit(
s.
charAt(
pos));
}
/**
* Converts digit at position {@code pos} in string {@code s} to integer or throws.
* @param s input string
* @param pos position (0-based)
* @return integer value of a digit at position pos
* @throws NumberFormatException if character at position pos is not an integer
*/
public static int
digitAt(
String s, int
pos) {
int
c =
s.
charAt(
pos) - '0';
if (
c < 0 ||
c > 9) {
throw new
NumberFormatException("Input string: \"" +
s + "\", position: " +
pos);
}
return
c;
}
/**
* @param c character
* @return true if the character is a whitespace character as defined in the backend's parser
*/
public static boolean
isSpace(char
c) {
return
c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\f';
}
/**
* @param c character
* @return true if the given character is a valid character for an operator in the backend's
* parser
*/
public static boolean
isOperatorChar(char
c) {
/*
* Extracted from operators defined by {self} and {op_chars}
* in pgsql/src/backend/parser/scan.l.
*/
return ",()[].;:+-*/%^<>=~!@#&|`?".
indexOf(
c) != -1;
}
/**
* Checks if a character is valid as the start of an identifier.
* PostgreSQL 9.4 allows column names like _, ‿, ⁀, ⁔, ︳, ︴, ﹍, ﹎, ﹏, _, so
* it is assumed isJavaIdentifierPart is good enough for PostgreSQL.
*
* @see <a href="https://www.postgresql.org/docs/9.6/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS">Identifiers and Key Words</a>
*
* @param c the character to check
* @return true if valid as first character of an identifier; false if not
*/
public static boolean
isIdentifierStartChar(char
c) {
/*
* PostgreSQL's implmementation is located in
* 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.
*/
return
Character.
isJavaIdentifierStart(
c);
}
/**
* Checks if a character is valid as the second or later character of an identifier.
*
* @param c the character to check
* @return true if valid as second or later character of an identifier; false if not
*/
public static boolean
isIdentifierContChar(char
c) {
return
Character.
isJavaIdentifierPart(
c);
}
/**
* @param c character
* @return true if the character terminates an identifier
*/
public static boolean
charTerminatesIdentifier(char
c) {
return
c == '"' ||
isSpace(
c) ||
isOperatorChar(
c);
}
/**
* Checks if a character is valid as the start of a dollar quoting tag.
*
* @param c the character to check
* @return true if valid as first character of a dollar quoting tag; false if not
*/
public static boolean
isDollarQuoteStartChar(char
c) {
/*
* The allowed dollar quote start and continuation characters
* must stay in sync with what the backend defines in
* pgsql/src/backend/parser/scan.l
*
* The quoted string starts with $foo$ where "foo" is an optional string
* in the form of an identifier, except that it may not contain "$",
* and extends to the first occurrence of an identical string.
* There is *no* processing of the quoted text.
*/
return
c != '$' &&
isIdentifierStartChar(
c);
}
/**
* Checks if a character is valid as the second or later character of a dollar quoting tag.
*
* @param c the character to check
* @return true if valid as second or later character of a dollar quoting tag; false if not
*/
public static boolean
isDollarQuoteContChar(char
c) {
return
c != '$' &&
isIdentifierContChar(
c);
}
/**
* Compares two sub-arrays of the given character array for equalness. If the length is zero, the
* result is true if and only if the offsets are within the bounds of the array.
*
* @param arr a char array
* @param offA first sub-array start offset
* @param offB second sub-array start offset
* @param len length of the sub arrays to compare
* @return true if the sub-arrays are equal; false if not
*/
private static boolean
subArraysEqual(final char[]
arr,
final int
offA, final int
offB,
final int
len) {
if (
offA < 0 ||
offB < 0
||
offA >=
arr.length ||
offB >=
arr.length
||
offA +
len >
arr.length ||
offB +
len >
arr.length) {
return false;
}
for (int
i = 0;
i <
len; ++
i) {
if (
arr[
offA +
i] !=
arr[
offB +
i]) {
return false;
}
}
return true;
}
/**
* Converts JDBC-specific callable statement escapes {@code { [? =] call <some_function> [(?,
* [?,..])] }} into the PostgreSQL format which is {@code select <some_function> (?, [?, ...]) as
* result} or {@code select * from <some_function> (?, [?, ...]) as result} (7.3)
*
* @param jdbcSql sql text with JDBC escapes
* @param stdStrings if backslash in single quotes should be regular character or escape one
* @param serverVersion server version
* @param protocolVersion protocol version
* @return SQL in appropriate for given server format
* @throws SQLException if given SQL is malformed
*/
public static
JdbcCallParseInfo modifyJdbcCall(
String jdbcSql, boolean
stdStrings,
int
serverVersion, int
protocolVersion) throws
SQLException {
// Mini-parser for JDBC function-call syntax (only)
// TODO: Merge with escape processing (and parameter parsing?) so we only parse each query once.
// RE: frequently used statements are cached (see {@link org.postgresql.jdbc.PgConnection#borrowQuery}), so this "merge" is not that important.
String sql =
jdbcSql;
boolean
isFunction = false;
boolean
outParamBeforeFunc = false;
int
len =
jdbcSql.
length();
int
state = 1;
boolean
inQuotes = false;
boolean
inEscape = false;
int
startIndex = -1;
int
endIndex = -1;
boolean
syntaxError = false;
int
i = 0;
while (
i <
len && !
syntaxError) {
char
ch =
jdbcSql.
charAt(
i);
switch (
state) {
case 1: // Looking for { at start of query
if (
ch == '{') {
++
i;
++
state;
} else if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
// Not function-call syntax. Skip the rest of the string.
i =
len;
}
break;
case 2: // After {, looking for ? or =, skipping whitespace
if (
ch == '?') {
outParamBeforeFunc =
isFunction = true; // { ? = call ... } -- function with one out parameter
++
i;
++
state;
} else if (
ch == 'c' ||
ch == 'C') { // { call ... } -- proc with no out parameters
state += 3; // Don't increase 'i'
} else if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
// "{ foo ...", doesn't make sense, complain.
syntaxError = true;
}
break;
case 3: // Looking for = after ?, skipping whitespace
if (
ch == '=') {
++
i;
++
state;
} else if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
syntaxError = true;
}
break;
case 4: // Looking for 'call' after '? =' skipping whitespace
if (
ch == 'c' ||
ch == 'C') {
++
state; // Don't increase 'i'.
} else if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
syntaxError = true;
}
break;
case 5: // Should be at 'call ' either at start of string or after ?=
if ((
ch == 'c' ||
ch == 'C') &&
i + 4 <=
len &&
jdbcSql.
substring(
i,
i + 4)
.
equalsIgnoreCase("call")) {
isFunction = true;
i += 4;
++
state;
} else if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
syntaxError = true;
}
break;
case 6: // Looking for whitespace char after 'call'
if (
Character.
isWhitespace(
ch)) {
// Ok, we found the start of the real call.
++
i;
++
state;
startIndex =
i;
} else {
syntaxError = true;
}
break;
case 7: // In "body" of the query (after "{ [? =] call ")
if (
ch == '\'') {
inQuotes = !
inQuotes;
++
i;
} else if (
inQuotes &&
ch == '\\' && !
stdStrings) {
// Backslash in string constant, skip next character.
i += 2;
} else if (!
inQuotes &&
ch == '{') {
inEscape = !
inEscape;
++
i;
} else if (!
inQuotes &&
ch == '}') {
if (!
inEscape) {
// Should be end of string.
endIndex =
i;
++
i;
++
state;
} else {
inEscape = false;
}
} else if (!
inQuotes &&
ch == ';') {
syntaxError = true;
} else {
// Everything else is ok.
++
i;
}
break;
case 8: // At trailing end of query, eating whitespace
if (
Character.
isWhitespace(
ch)) {
++
i;
} else {
syntaxError = true;
}
break;
default:
throw new
IllegalStateException("somehow got into bad state " +
state);
}
}
// We can only legally end in a couple of states here.
if (
i ==
len && !
syntaxError) {
if (
state == 1) {
// Not an escaped syntax.
return new
JdbcCallParseInfo(
sql,
isFunction);
}
if (
state != 8) {
syntaxError = true; // Ran out of query while still parsing
}
}
if (
syntaxError) {
throw new
PSQLException(
GT.
tr("Malformed function or procedure escape syntax at offset {0}.",
i),
PSQLState.
STATEMENT_NOT_ALLOWED_IN_FUNCTION_CALL);
}
String prefix = "select * from ";
String suffix = " as result";
String s =
jdbcSql.
substring(
startIndex,
endIndex);
int
prefixLength =
prefix.
length();
StringBuilder sb = new
StringBuilder(
prefixLength +
jdbcSql.
length() +
suffix.
length() + 10);
sb.
append(
prefix);
sb.
append(
s);
int
opening =
s.
indexOf('(') + 1;
if (
opening == 0) {
// here the function call has no parameters declaration eg : "{ ? = call pack_getValue}"
sb.
append(
outParamBeforeFunc ? "(?)" : "()");
} else if (
outParamBeforeFunc) {
// move the single out parameter into the function call
// so that it can be treated like all other parameters
boolean
needComma = false;
// the following loop will check if the function call has parameters
// eg "{ ? = call pack_getValue(?) }" vs "{ ? = call pack_getValue() }
for (int
j =
opening +
prefixLength;
j <
sb.
length();
j++) {
char
c =
sb.
charAt(
j);
if (
c == ')') {
break;
}
if (!
Character.
isWhitespace(
c)) {
needComma = true;
break;
}
}
// insert the return parameter as the first parameter of the function call
if (
needComma) {
sb.
insert(
opening +
prefixLength, "?,");
} else {
sb.
insert(
opening +
prefixLength, "?");
}
}
sql =
sb.
append(
suffix).
toString();
return new
JdbcCallParseInfo(
sql,
isFunction);
}
/**
* <p>Filter the SQL string of Java SQL Escape clauses.</p>
*
* <p>Currently implemented Escape clauses are those mentioned in 11.3 in the specification.
* Basically we look through the sql string for {d xxx}, {t xxx}, {ts xxx}, {oj xxx} or {fn xxx}
* in non-string sql code. When we find them, we just strip the escape part leaving only the xxx
* part. So, something like "select * from x where d={d '2001-10-09'}" would return "select * from
* x where d= '2001-10-09'".</p>
*
* @param p_sql the original query text
* @param replaceProcessingEnabled whether replace_processing_enabled is on
* @param standardConformingStrings whether standard_conforming_strings is on
* @return PostgreSQL-compatible SQL
* @throws SQLException if given SQL is wrong
*/
public static
String replaceProcessing(
String p_sql, boolean
replaceProcessingEnabled,
boolean
standardConformingStrings) throws
SQLException {
if (
replaceProcessingEnabled) {
// Since escape codes can only appear in SQL CODE, we keep track
// of if we enter a string or not.
int
len =
p_sql.
length();
char[]
chars =
p_sql.
toCharArray();
StringBuilder newsql = new
StringBuilder(
len);
int
i = 0;
while (
i <
len) {
i =
parseSql(
chars,
i,
newsql, false,
standardConformingStrings);
// We need to loop here in case we encounter invalid
// SQL, consider: SELECT a FROM t WHERE (1 > 0)) ORDER BY a
// We can't ending replacing after the extra closing paren
// because that changes a syntax error to a valid query
// that isn't what the user specified.
if (
i <
len) {
newsql.
append(
chars[
i]);
i++;
}
}
return
newsql.
toString();
} else {
return
p_sql;
}
}
/**
* parse the given sql from index i, appending it to the given buffer until we hit an unmatched
* right parentheses or end of string. When the stopOnComma flag is set we also stop processing
* when a comma is found in sql text that isn't inside nested parenthesis.
*
* @param p_sql the original query text
* @param i starting position for replacing
* @param newsql where to write the replaced output
* @param stopOnComma should we stop after hitting the first comma in sql text?
* @param stdStrings whether standard_conforming_strings is on
* @return the position we stopped processing at
* @throws SQLException if given SQL is wrong
*/
private static int
parseSql(char[]
p_sql, int
i,
StringBuilder newsql, boolean
stopOnComma,
boolean
stdStrings) throws
SQLException {
SqlParseState state =
SqlParseState.
IN_SQLCODE;
int
len =
p_sql.length;
int
nestedParenthesis = 0;
boolean
endOfNested = false;
// because of the ++i loop
i--;
while (!
endOfNested && ++
i <
len) {
char
c =
p_sql[
i];
state_switch:
switch (
state) {
case
IN_SQLCODE:
if (
c == '$') {
int
i0 =
i;
i =
parseDollarQuotes(
p_sql,
i);
checkParsePosition(
i,
len,
i0,
p_sql,
"Unterminated dollar quote started at position {0} in SQL {1}. Expected terminating $$");
newsql.
append(
p_sql,
i0,
i -
i0 + 1);
break;
} else if (
c == '\'') {
// start of a string?
int
i0 =
i;
i =
parseSingleQuotes(
p_sql,
i,
stdStrings);
checkParsePosition(
i,
len,
i0,
p_sql,
"Unterminated string literal started at position {0} in SQL {1}. Expected ' char");
newsql.
append(
p_sql,
i0,
i -
i0 + 1);
break;
} else if (
c == '"') {
// start of a identifier?
int
i0 =
i;
i =
parseDoubleQuotes(
p_sql,
i);
checkParsePosition(
i,
len,
i0,
p_sql,
"Unterminated identifier started at position {0} in SQL {1}. Expected \" char");
newsql.
append(
p_sql,
i0,
i -
i0 + 1);
break;
} else if (
c == '/') {
int
i0 =
i;
i =
parseBlockComment(
p_sql,
i);
checkParsePosition(
i,
len,
i0,
p_sql,
"Unterminated block comment started at position {0} in SQL {1}. Expected */ sequence");
newsql.
append(
p_sql,
i0,
i -
i0 + 1);
break;
} else if (
c == '-') {
int
i0 =
i;
i =
parseLineComment(
p_sql,
i);
newsql.
append(
p_sql,
i0,
i -
i0 + 1);
break;
} else if (
c == '(') { // begin nested sql
nestedParenthesis++;
} else if (
c == ')') { // end of nested sql
nestedParenthesis--;
if (
nestedParenthesis < 0) {
endOfNested = true;
break;
}
} else if (
stopOnComma &&
c == ',' &&
nestedParenthesis == 0) {
endOfNested = true;
break;
} else if (
c == '{') { // start of an escape code?
if (
i + 1 <
len) {
SqlParseState[]
availableStates =
SqlParseState.
VALUES;
// skip first state, it's not a escape code state
for (int
j = 1;
j <
availableStates.length;
j++) {
SqlParseState availableState =
availableStates[
j];
int
matchedPosition =
availableState.
getMatchedPosition(
p_sql,
i + 1);
if (
matchedPosition == 0) {
continue;
}
i +=
matchedPosition;
if (
availableState.
replacementKeyword != null) {
newsql.
append(
availableState.
replacementKeyword);
}
state =
availableState;
break
state_switch;
}
}
}
newsql.
append(
c);
break;
case
ESC_FUNCTION:
// extract function name
i =
escapeFunction(
p_sql,
i,
newsql,
stdStrings);
state =
SqlParseState.
IN_SQLCODE; // end of escaped function (or query)
break;
case
ESC_DATE:
case
ESC_TIME:
case
ESC_TIMESTAMP:
case
ESC_OUTERJOIN:
case
ESC_ESCAPECHAR:
if (
c == '}') {
state =
SqlParseState.
IN_SQLCODE; // end of escape code.
} else {
newsql.
append(
c);
}
break;
} // end switch
}
return
i;
}
private static int
findOpenBrace(char[]
p_sql, int
i) {
int
posArgs;
for (
posArgs =
i;
posArgs <
p_sql.length &&
p_sql[
posArgs] != '(';
posArgs++) {
;
}
return
posArgs;
}
private static void
checkParsePosition(int
i, int
len, int
i0, char[]
p_sql,
String message)
throws
PSQLException {
if (
i <
len) {
return;
}
throw new
PSQLException(
GT.
tr(
message,
i0, new
String(
p_sql)),
PSQLState.
SYNTAX_ERROR);
}
private static int
escapeFunction(char[]
p_sql, int
i,
StringBuilder newsql, boolean
stdStrings) throws
SQLException {
String functionName;
int
argPos =
findOpenBrace(
p_sql,
i);
if (
argPos <
p_sql.length) {
functionName = new
String(
p_sql,
i,
argPos -
i).
trim();
// extract arguments
i =
argPos + 1;// we start the scan after the first (
i =
escapeFunctionArguments(
newsql,
functionName,
p_sql,
i,
stdStrings);
}
// go to the end of the function copying anything found
i++;
while (
i <
p_sql.length &&
p_sql[
i] != '}') {
newsql.
append(
p_sql[
i++]);
}
return
i;
}
/**
* Generate sql for escaped functions.
*
* @param newsql destination StringBuilder
* @param functionName the escaped function name
* @param p_sql input SQL text (containing arguments of a function call with possible JDBC escapes)
* @param i position in the input SQL
* @param stdStrings whether standard_conforming_strings is on
* @return the right PostgreSQL sql
* @throws SQLException if something goes wrong
*/
private static int
escapeFunctionArguments(
StringBuilder newsql,
String functionName, char[]
p_sql, int
i,
boolean
stdStrings)
throws
SQLException {
// Maximum arity of functions in EscapedFunctions is 3
List<
CharSequence>
parsedArgs = new
ArrayList<
CharSequence>(3);
while (true) {
StringBuilder arg = new
StringBuilder();
int
lastPos =
i;
i =
parseSql(
p_sql,
i,
arg, true,
stdStrings);
if (
i !=
lastPos) {
parsedArgs.
add(
arg);
}
if (
i >=
p_sql.length // should not happen
||
p_sql[
i] != ',') {
break;
}
i++;
}
Method method =
EscapedFunctions2.
getFunction(
functionName);
if (
method == null) {
newsql.
append(
functionName);
EscapedFunctions2.
appendCall(
newsql, "(", ",", ")",
parsedArgs);
return
i;
}
try {
method.
invoke(null,
newsql,
parsedArgs);
} catch (
InvocationTargetException e) {
Throwable targetException =
e.
getTargetException();
if (
targetException instanceof
SQLException) {
throw (
SQLException)
targetException;
} else {
throw new
PSQLException(
targetException.
getMessage(),
PSQLState.
SYSTEM_ERROR);
}
} catch (
IllegalAccessException e) {
throw new
PSQLException(
e.
getMessage(),
PSQLState.
SYSTEM_ERROR);
}
return
i;
}
private static final char[]
QUOTE_OR_ALPHABETIC_MARKER = {'\"', '0'};
private static final char[]
QUOTE_OR_ALPHABETIC_MARKER_OR_PARENTHESIS = {'\"', '0', '('};
private static final char[]
SINGLE_QUOTE = {'\''};
// Static variables for parsing SQL when replaceProcessing is true.
private enum
SqlParseState {
IN_SQLCODE,
ESC_DATE("d",
SINGLE_QUOTE, "DATE "),
ESC_TIME("t",
SINGLE_QUOTE, "TIME "),
ESC_TIMESTAMP("ts",
SINGLE_QUOTE, "TIMESTAMP "),
ESC_FUNCTION("fn",
QUOTE_OR_ALPHABETIC_MARKER, null),
ESC_OUTERJOIN("oj",
QUOTE_OR_ALPHABETIC_MARKER_OR_PARENTHESIS, null),
ESC_ESCAPECHAR("escape",
SINGLE_QUOTE, "ESCAPE ");
private static final
SqlParseState[]
VALUES =
values();
private final char[]
escapeKeyword;
private final char[]
allowedValues;
private final
String replacementKeyword;
SqlParseState() {
this("", new char[0], null);
}
SqlParseState(
String escapeKeyword, char[]
allowedValues,
String replacementKeyword) {
this.
escapeKeyword =
escapeKeyword.
toCharArray();
this.
allowedValues =
allowedValues;
this.
replacementKeyword =
replacementKeyword;
}
private boolean
startMatches(char[]
p_sql, int
pos) {
// check for the keyword
for (char
c :
escapeKeyword) {
if (
pos >=
p_sql.length) {
return false;
}
char
curr =
p_sql[
pos++];
if (
curr !=
c &&
curr !=
Character.
toUpperCase(
c)) {
return false;
}
}
return
pos <
p_sql.length;
}
private int
getMatchedPosition(char[]
p_sql, int
pos) {
// check for the keyword
if (!
startMatches(
p_sql,
pos)) {
return 0;
}
int
newPos =
pos +
escapeKeyword.length;
// check for the beginning of the value
char
curr =
p_sql[
newPos];
// ignore any in-between whitespace
while (
curr == ' ') {
newPos++;
if (
newPos >=
p_sql.length) {
return 0;
}
curr =
p_sql[
newPos];
}
for (char
c :
allowedValues) {
if (
curr ==
c || (
c == '0' &&
Character.
isLetter(
curr))) {
return
newPos -
pos;
}
}
return 0;
}
}
}