diff --git a/pgjdbc/src/main/java/org/postgresql/core/Parser.java b/pgjdbc/src/main/java/org/postgresql/core/Parser.java index ec084591a5..7e88be09a4 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/Parser.java +++ b/pgjdbc/src/main/java/org/postgresql/core/Parser.java @@ -22,6 +22,7 @@ /** * 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) @@ -961,7 +962,7 @@ public static JdbcCallParseInfo modifyJdbcCall(String jdbcSql, boolean stdString * @param standardConformingStrings whether standard_conforming_strings is on * @return PostgreSQL-compatible SQL */ - static String replaceProcessing(String p_sql, boolean replaceProcessingEnabled, + 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 @@ -1012,6 +1013,8 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s i--; while (!endOfNested && ++i < len) { char c = p_sql[i]; + + state_switch: switch (state) { case IN_SQLCODE: if (c == '$') { @@ -1054,37 +1057,20 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s break; } else if (c == '{') { // start of an escape code? if (i + 1 < len) { - char next = p_sql[i + 1]; - char nextnext = (i + 2 < len) ? p_sql[i + 2] : '\0'; - if (next == 'd' || next == 'D') { - state = SqlParseState.ESC_TIMEDATE; - i++; - newsql.append("DATE "); - break; - } else if (next == 't' || next == 'T') { - state = SqlParseState.ESC_TIMEDATE; - if (nextnext == 's' || nextnext == 'S') { - // timestamp constant - i += 2; - newsql.append("TIMESTAMP "); - } else { - // time constant - i++; - newsql.append("TIME "); + 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); } - break; - } else if (next == 'f' || next == 'F') { - state = SqlParseState.ESC_FUNCTION; - i += (nextnext == 'n' || nextnext == 'N') ? 2 : 1; - break; - } else if (next == 'o' || next == 'O') { - state = SqlParseState.ESC_OUTERJOIN; - i += (nextnext == 'j' || nextnext == 'J') ? 2 : 1; - break; - } else if (next == 'e' || next == 'E') { - // we assume that escape is the only escape sequence beginning with e - state = SqlParseState.ESC_ESCAPECHAR; - break; + state = availableState; + break state_switch; } } } @@ -1114,7 +1100,9 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s } state = SqlParseState.IN_SQLCODE; // end of escaped function (or query) break; - case ESC_TIMEDATE: + case ESC_DATE: + case ESC_TIME: + case ESC_TIMESTAMP: case ESC_OUTERJOIN: case ESC_ESCAPECHAR: if (c == '}') { @@ -1178,12 +1166,67 @@ private static String escapeFunction(String functionName, String args, boolean s } } + private final static char[] QUOTE_OR_ALPHABETIC_MARKER = new char[]{'\"', '0'}; + private final static char[] SINGLE_QUOTE = new char[]{'\''}; + // Static variables for parsing SQL when replaceProcessing is true. private enum SqlParseState { IN_SQLCODE, - ESC_TIMEDATE, - ESC_FUNCTION, - ESC_OUTERJOIN, - ESC_ESCAPECHAR; + 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, null), + ESC_ESCAPECHAR("escape", SINGLE_QUOTE, "ESCAPE "); + + 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 int getMatchedPosition(char[] p_sql, int pos) { + int newPos = pos; + + // check for the keyword + for (char c : escapeKeyword) { + if (newPos >= p_sql.length) { + return 0; + } + char curr = p_sql[newPos++]; + if (curr != c && curr != Character.toUpperCase(c)) { + return 0; + } + } + if (newPos >= p_sql.length) { + return 0; + } + + // 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; + } } } diff --git a/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java b/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java index 3edb18f785..68bdf42788 100644 --- a/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java +++ b/pgjdbc/src/test/java/org/postgresql/core/ParserTest.java @@ -103,4 +103,29 @@ public void testSelectCommandParsing() { "select".getChars(0, 6, command, 0); Assert.assertTrue("Failed to correctly parse lower case command.", Parser.parseSelectKeyword(command, 0)); } + + public void testEscapeProcessing() throws Exception { + Assert.assertEquals("DATE '1999-01-09'", Parser.replaceProcessing("{d '1999-01-09'}", true, false)); + Assert.assertEquals("DATE '1999-01-09'", Parser.replaceProcessing("{D '1999-01-09'}", true, false)); + Assert.assertEquals("TIME '20:00:03'", Parser.replaceProcessing("{t '20:00:03'}", true, false)); + Assert.assertEquals("TIME '20:00:03'", Parser.replaceProcessing("{T '20:00:03'}", true, false)); + Assert.assertEquals("TIMESTAMP '1999-01-09 20:11:11.123455'", Parser.replaceProcessing("{ts '1999-01-09 20:11:11.123455'}", true, false)); + Assert.assertEquals("TIMESTAMP '1999-01-09 20:11:11.123455'", Parser.replaceProcessing("{Ts '1999-01-09 20:11:11.123455'}", true, false)); + + Assert.assertEquals("user", Parser.replaceProcessing("{fn user()}", true, false)); + Assert.assertEquals("cos(1)", Parser.replaceProcessing("{fn cos(1)}", true, false)); + Assert.assertEquals("extract(week from DATE '2005-01-24')", Parser.replaceProcessing("{fn week({d '2005-01-24'})}", true, false)); + + Assert.assertEquals("\"T1\" LEFT OUTER JOIN t2 ON \"T1\".id = t2.id", + Parser.replaceProcessing("{oj \"T1\" LEFT OUTER JOIN t2 ON \"T1\".id = t2.id}", true, false)); + + Assert.assertEquals("ESCAPE '_'", Parser.replaceProcessing("{escape '_'}", true, false)); + + // nothing should be changed in that case, no valid escape code + Assert.assertEquals("{obj : 1}", Parser.replaceProcessing("{obj : 1}", true, false)); + } + + public void testUnterminatedEscape() throws Exception { + Assert.assertEquals("{oj ", Parser.replaceProcessing("{oj ", true, false)); + } } diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java index 44d42931c2..00b6950843 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java @@ -109,7 +109,7 @@ public void testPrepareCall() { public void testNativeSQL() throws Exception { // test a simple escape con = TestUtil.openDB(); - assertEquals("DATE '2005-01-24'", con.nativeSQL("{d '2005-01-24'}")); + assertEquals("DATE '2005-01-24'", con.nativeSQL("{d '2005-01-24'}")); } /* diff --git a/ubenchmark/src/main/java/org/postgresql/benchmark/escaping/EscapeProcessing.java b/ubenchmark/src/main/java/org/postgresql/benchmark/escaping/EscapeProcessing.java new file mode 100644 index 0000000000..7b6a514637 --- /dev/null +++ b/ubenchmark/src/main/java/org/postgresql/benchmark/escaping/EscapeProcessing.java @@ -0,0 +1,33 @@ +package org.postgresql.benchmark.escaping; + +import org.postgresql.core.Parser; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@State(Scope.Thread) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class EscapeProcessing { + + private String fnEscapeSQL = "{fn week({d '2005-01-24'})}"; + private boolean replaceProcessingEnabled = true; + private boolean standardConformingStrings = false; + + @Benchmark + public String escapeFunctionWithDate() throws Exception { + return Parser.replaceProcessing(fnEscapeSQL, replaceProcessingEnabled, standardConformingStrings); + } +}