/*
 * 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), Christopher Deckers (chrriis@gmail.com)
/** * 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.
Params:
  • query – jdbc query to parse
  • standardConformingStrings – whether to allow backslashes to be used as escape characters in single quote literals
  • withParameters – whether to replace ?, ? with $1, $2, etc
  • splitStatements – whether to split statements by semicolon
  • isBatchedReWriteConfigured – whether re-write optimization is enabled
  • returningColumnNames – for simple insert, update, delete add returning with given column names
Throws:
  • SQLException – if unable to add returning clause (invalid column names)
Returns:list of native queries
/** * 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 List<Integer> to int[]. Empty and null lists are converted to empty array.
Params:
  • list – input list
Returns:output array
/** * 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; }

Find the end of the single-quoted string starting at the given offset.

Note: for 'single '' quote in string', this method currently returns the offset of first ' character after the initial one. The caller must call the method a second time for the second part of the quoted string.

Params:
  • query – query
  • offset – start offset
  • standardConformingStrings – standard conforming strings
Returns:position of the end of the single-quoted string
/** * <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; }

Find the end of the double-quoted string starting at the given offset.

Note: for "double "" quote in string", this method currently returns the offset of first " character after the initial one. The caller must call the method a second time for the second part of the quoted string.

Params:
  • query – query
  • offset – start offset
Returns:position of the end of the double-quoted string
/** * <p>Find the end of the double-quoted string starting at the given offset.</p> * * <p>Note: for <tt>&quot;double &quot;&quot; quote in string&quot;</tt>, this method currently * returns the offset of first <tt>&quot;</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] != '"') { // do nothing } return offset; }
Test if the dollar character ($) at the given offset starts a dollar-quoted string and return the offset of the ending dollar character.
Params:
  • query – query
  • offset – start offset
Returns:offset of the ending dollar character
/** * 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 - character at offset starts a -- style line comment, and return the position of the first \r or \n character.
Params:
  • query – query
  • offset – start offset
Returns:position of the first \r or \n character
/** * 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 / character at offset starts a block comment, and return the position of the last / character.
Params:
  • query – query
  • offset – start offset
Returns:position of the last / character
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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 Long.parseLong(String) when parsing a substring is required
Params:
  • s – string to parse
  • beginIndex – begin index
  • endIndex – end index
Returns:long value
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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.
Params:
  • query – char[] of the query statement
  • offset – position of query to start checking
Returns:boolean indicates presence of word
/** * 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 s has digit at position pos.
Params:
  • s – input string
  • pos – position (0-based)
Returns:true if input string s has digit at position pos
/** * 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 pos in string s to integer or throws.
Params:
  • s – input string
  • pos – position (0-based)
Throws:
Returns:integer value of a digit at position 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; }
Params:
  • c – character
Returns:true if the character is a whitespace character as defined in the backend's parser
/** * @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'; }
Params:
  • c – character
Returns:true if the given character is a valid character for an operator in the backend's parser
/** * @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.
Params:
  • c – the character to check
See Also:
Returns:true if valid as first character of an identifier; false if not
/** * 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. * * @param c the character to check * @return true if valid as first character of an identifier; false if not * @see <a href="https://www.postgresql.org/docs/9.6/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS">Identifiers and Key Words</a> */
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.
Params:
  • c – the character to check
Returns:true if valid as second or later character of an identifier; false if not
/** * 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); }
Params:
  • c – character
Returns:true if the character terminates an identifier
/** * @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.
Params:
  • c – the character to check
Returns:true if valid as first character of a dollar quoting tag; false if not
/** * 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.
Params:
  • c – the character to check
Returns:true if valid as second or later character of a dollar quoting tag; false if not
/** * 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.
Params:
  • arr – a char array
  • offA – first sub-array start offset
  • offB – second sub-array start offset
  • len – length of the sub arrays to compare
Returns:true if the sub-arrays are equal; false if not
/** * 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 { [? =] call <some_function> [(?, [?,..])] } into the PostgreSQL format which is select <some_function> (?, [?, ...]) as result or select * from <some_function> (?, [?, ...]) as result (7.3)
Params:
  • jdbcSql – sql text with JDBC escapes
  • stdStrings – if backslash in single quotes should be regular character or escape one
  • serverVersion – server version
  • protocolVersion – protocol version
Throws:
Returns:SQL in appropriate for given server format
/** * 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); }

Filter the SQL string of Java SQL Escape clauses.

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'".

Params:
  • sql – the original query text
  • replaceProcessingEnabled – whether replace_processing_enabled is on
  • standardConformingStrings – whether standard_conforming_strings is on
Throws:
Returns:PostgreSQL-compatible SQL
/** * <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 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 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 = sql.length(); char[] chars = 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 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.
Params:
  • sql – the original query text
  • i – starting position for replacing
  • newsql – where to write the replaced output
  • stopOnComma – should we stop after hitting the first comma in sql text?
  • stdStrings – whether standard_conforming_strings is on
Throws:
Returns:the position we stopped processing at
/** * 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 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[] sql, int i, StringBuilder newsql, boolean stopOnComma, boolean stdStrings) throws SQLException { SqlParseState state = SqlParseState.IN_SQLCODE; int len = sql.length; int nestedParenthesis = 0; boolean endOfNested = false; // because of the ++i loop i--; while (!endOfNested && ++i < len) { char c = sql[i]; state_switch: switch (state) { case IN_SQLCODE: if (c == '$') { int i0 = i; i = parseDollarQuotes(sql, i); checkParsePosition(i, len, i0, sql, "Unterminated dollar quote started at position {0} in SQL {1}. Expected terminating $$"); newsql.append(sql, i0, i - i0 + 1); break; } else if (c == '\'') { // start of a string? int i0 = i; i = parseSingleQuotes(sql, i, stdStrings); checkParsePosition(i, len, i0, sql, "Unterminated string literal started at position {0} in SQL {1}. Expected ' char"); newsql.append(sql, i0, i - i0 + 1); break; } else if (c == '"') { // start of a identifier? int i0 = i; i = parseDoubleQuotes(sql, i); checkParsePosition(i, len, i0, sql, "Unterminated identifier started at position {0} in SQL {1}. Expected \" char"); newsql.append(sql, i0, i - i0 + 1); break; } else if (c == '/') { int i0 = i; i = parseBlockComment(sql, i); checkParsePosition(i, len, i0, sql, "Unterminated block comment started at position {0} in SQL {1}. Expected */ sequence"); newsql.append(sql, i0, i - i0 + 1); break; } else if (c == '-') { int i0 = i; i = parseLineComment(sql, i); newsql.append(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(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(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[] sql, int i) { int posArgs = i; while (posArgs < sql.length && sql[posArgs] != '(') { posArgs++; } return posArgs; } private static void checkParsePosition(int i, int len, int i0, char[] sql, String message) throws PSQLException { if (i < len) { return; } throw new PSQLException( GT.tr(message, i0, new String(sql)), PSQLState.SYNTAX_ERROR); } private static int escapeFunction(char[] sql, int i, StringBuilder newsql, boolean stdStrings) throws SQLException { String functionName; int argPos = findOpenBrace(sql, i); if (argPos < sql.length) { functionName = new String(sql, i, argPos - i).trim(); // extract arguments i = argPos + 1;// we start the scan after the first ( i = escapeFunctionArguments(newsql, functionName, sql, i, stdStrings); } // go to the end of the function copying anything found i++; while (i < sql.length && sql[i] != '}') { newsql.append(sql[i++]); } return i; }
Generate sql for escaped functions.
Params:
  • newsql – destination StringBuilder
  • functionName – the escaped function name
  • sql – input SQL text (containing arguments of a function call with possible JDBC escapes)
  • i – position in the input SQL
  • stdStrings – whether standard_conforming_strings is on
Throws:
Returns:the right PostgreSQL sql
/** * Generate sql for escaped functions. * * @param newsql destination StringBuilder * @param functionName the escaped function name * @param 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[] 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(sql, i, arg, true, stdStrings); if (i != lastPos) { parsedArgs.add(arg); } if (i >= sql.length // should not happen || 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[] sql, int pos) { // check for the keyword for (char c : escapeKeyword) { if (pos >= sql.length) { return false; } char curr = sql[pos++]; if (curr != c && curr != Character.toUpperCase(c)) { return false; } } return pos < sql.length; } private int getMatchedPosition(char[] sql, int pos) { // check for the keyword if (!startMatches(sql, pos)) { return 0; } int newPos = pos + escapeKeyword.length; // check for the beginning of the value char curr = sql[newPos]; // ignore any in-between whitespace while (curr == ' ') { newPos++; if (newPos >= sql.length) { return 0; } curr = sql[newPos]; } for (char c : allowedValues) { if (curr == c || (c == '0' && Character.isLetter(curr))) { return newPos - pos; } } return 0; } } }