Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails - Mailing list pgsql-jdbc
| From | Michael Paesold |
|---|---|
| Subject | Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails |
| Date | |
| Msg-id | 45491601.7050407@gmx.at Whole thread Raw |
| In response to | Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails (Michael Paesold <mpaesold@gmx.at>) |
| Responses |
Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails
|
| List | pgsql-jdbc |
Michael Paesold wrote:
> Kris Jurka wrote:
>> On Fri, 6 Oct 2006, Michael Paesold wrote:
>>
>>> I thought I'd keep you posted on my progress. Here is an updated
>>> work-in-progress patch implementing dollar-quotes, -- and /* */
>>> quotes (also with SQL compliant nesting, i.e. /* /* */ */). It
>>> replaces my last one.
>>
>> This looks good to me. Have you done anything further?
>
> Yeah, I have started extracting the code for parsing specific parts of
> the query (single-quotes, dollar-quotes, comments, etc.) into a separate
> class so the code can be reused in the V2Query code. I was also looking
> at supporting standard_conforming_strings as a patch on top of that work.
Attached is a new version of the patch. From my side it looks pretty
complete now, but I am of course willing to improve it based on further
comments.
The parsing code is now split into several methods in a new class
org.postgresql.core.Parser. These methods are used in the v3
QueryExecutorImpl as well as the V2Query class. Therefore, both support
dollar-quotes and comments, now.
If you are OK with the approach, I will start coding the support for
standard_conforming_strings.
Best Regards,
Michael Paesold
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/core/Parser.java pgjdbc.323e7d139a7b/org/postgresql/core/Parser.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/Parser.java 1970-01-01 01:00:00.000000000 +0100
--- pgjdbc.323e7d139a7b/org/postgresql/core/Parser.java 2006-11-01 22:41:41.000000000 +0100
***************
*** 0 ****
--- 1,199 ----
+ /*-------------------------------------------------------------------------
+ *
+ * Copyright (c) 2006, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * $PostgreSQL$
+ *
+ *-------------------------------------------------------------------------
+ */
+ package org.postgresql.core;
+
+ /**
+ * Basic query parser infrastructure.
+ *
+ * @author Michael Paesold (mpaesold@gmx.at)
+ */
+ public class Parser {
+
+ /**
+ * Find the end of the single-quoted string starting at the given offset.
+ */
+ public static int parseSingleQuotes(final char[] query, int offset) {
+ while (++offset < query.length)
+ {
+ switch (query[offset])
+ {
+ case '\\':
+ ++offset;
+ break;
+ case '\'':
+ return offset;
+ }
+ }
+ return query.length;
+ }
+
+ /**
+ * Find the end of the double-quoted string starting at the given offset.
+ */
+ 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.
+ */
+ public static int parseDollarQuotes(final char[] query, int offset) {
+ if (offset + 1 < query.length)
+ {
+ 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, 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.
+ */
+ public static int parseLineComment(final char[] query, int offset) {
+ if (offset + 1 < query.length && query[offset + 1] == '-')
+ {
+ while (++offset < query.length)
+ {
+ 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.
+ */
+ 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;
+ }
+ if (level == 0)
+ {
+ --offset; // reset position to last '/' char
+ break;
+ }
+ }
+ }
+ return offset;
+ }
+
+ /**
+ * 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
+ */
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+ || c == '_' || c > 127;
+ }
+
+ /**
+ * Checks if a character is valid as the second or latter 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 >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+ || c == '_' || c > 127
+ || (c >= '0' && c <= '9');
+ }
+
+ /**
+ * 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;
+ }
+ }
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/core/v2/V2Query.java
pgjdbc.323e7d139a7b/org/postgresql/core/v2/V2Query.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/v2/V2Query.java 2006-11-01 22:41:41.000000000 +0100
--- pgjdbc.323e7d139a7b/org/postgresql/core/v2/V2Query.java 2006-11-01 22:41:41.000000000 +0100
***************
*** 29,62 ****
Vector v = new Vector();
int lastParmEnd = 0;
! boolean inSingleQuotes = false;
! boolean inDoubleQuotes = false;
! for (int i = 0; i < query.length(); ++i)
{
! char c = query.charAt(i);
!
! switch (c)
{
! case '\\':
! if (inSingleQuotes)
! ++i; // Skip one character.
break;
! case '\'':
! inSingleQuotes = !inDoubleQuotes && !inSingleQuotes;
break;
! case '"':
! inDoubleQuotes = !inSingleQuotes && !inDoubleQuotes;
break;
case '?':
! if (!inSingleQuotes && !inDoubleQuotes)
! {
! v.addElement(query.substring (lastParmEnd, i));
! lastParmEnd = i + 1;
! }
break;
default:
--- 29,63 ----
Vector v = new Vector();
int lastParmEnd = 0;
! char []aChars = query.toCharArray();
! for (int i = 0; i < aChars.length; ++i)
{
! switch (aChars[i])
{
! case '\'': // single-quotes
! i = Parser.parseSingleQuotes(aChars, i);
! 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 '?':
! v.addElement(query.substring (lastParmEnd, i));
! lastParmEnd = i + 1;
break;
default:
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/core/v3/QueryExecutorImpl.java
pgjdbc.323e7d139a7b/org/postgresql/core/v3/QueryExecutorImpl.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/v3/QueryExecutorImpl.java 2006-11-01 22:41:41.000000000 +0100
--- pgjdbc.323e7d139a7b/org/postgresql/core/v3/QueryExecutorImpl.java 2006-11-01 22:41:41.000000000 +0100
***************
*** 63,116 ****
ArrayList statementList = new ArrayList();
ArrayList fragmentList = new ArrayList(15);
- boolean inQuotes = false;
int fragmentStart = 0;
-
- boolean inSingleQuotes = false;
- boolean inDoubleQuotes = false;
int inParen = 0;
!
char []aChars = query.toCharArray();
!
for (int i = 0; i < aChars.length; ++i)
{
! char c = aChars[i];
!
! switch (c)
{
! case '\\':
! if (inSingleQuotes)
! ++i; // Skip one character.
break;
! case '\'':
! inSingleQuotes = !inDoubleQuotes && !inSingleQuotes;
break;
! case '"':
! inDoubleQuotes = !inSingleQuotes && !inDoubleQuotes;
break;
! case '?':
! if (withParameters && !inSingleQuotes && !inDoubleQuotes)
! {
! fragmentList.add(query.substring(fragmentStart, i));
! fragmentStart = i + 1;
! }
break;
case '(':
! if (!inSingleQuotes && !inDoubleQuotes)
! inParen++;
break;
case ')':
! if (!inSingleQuotes && !inDoubleQuotes)
! inParen--;
break;
case ';':
! if (!inSingleQuotes && !inDoubleQuotes && inParen == 0)
{
fragmentList.add(query.substring(fragmentStart, i));
fragmentStart = i + 1;
--- 63,115 ----
ArrayList statementList = new ArrayList();
ArrayList fragmentList = new ArrayList(15);
int fragmentStart = 0;
int inParen = 0;
!
char []aChars = query.toCharArray();
!
for (int i = 0; i < aChars.length; ++i)
{
! switch (aChars[i])
{
! case '\'': // single-quotes
! i = Parser.parseSingleQuotes(aChars, i);
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 '(':
! inParen++;
break;
case ')':
! inParen--;
! break;
!
! case '?':
! if (withParameters)
! {
! fragmentList.add(query.substring(fragmentStart, i));
! fragmentStart = i + 1;
! }
break;
case ';':
! if (inParen == 0)
{
fragmentList.add(query.substring(fragmentStart, i));
fragmentStart = i + 1;
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/PreparedStatementTest.java
pgjdbc.323e7d139a7b/org/postgresql/test/jdbc2/PreparedStatementTest.java
*** pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/PreparedStatementTest.java 2006-11-01 22:41:41.000000000 +0100
--- pgjdbc.323e7d139a7b/org/postgresql/test/jdbc2/PreparedStatementTest.java 2006-11-01 22:41:41.000000000 +0100
***************
*** 304,309 ****
--- 304,374 ----
pstmt.close();
}
}
+
+ public void testDollarQuotes() throws SQLException {
+ // dollar-quotes are supported in the backend since version 8.0
+ if (!TestUtil.haveMinimumServerVersion(conn, "8.0"))
+ return;
+
+ PreparedStatement st;
+ ResultSet rs;
+
+ st = conn.prepareStatement("SELECT $$;$$ WHERE $x$?$x$=$_0$?$_0$ AND $$?$$=?");
+ st.setString(1, "?");
+ rs = st.executeQuery();
+ assertTrue(rs.next());
+ assertEquals(";", rs.getString(1));
+ assertFalse(rs.next());
+ st.close();
+
+ st = conn.prepareStatement(
+ "SELECT $__$;$__$ WHERE ''''=$q_1$'$q_1$ AND ';'=?;"
+ + "SELECT $x$$a$;$x $a$$x$ WHERE $$;$$=? OR ''=$c$c$;$c$;"
+ + "SELECT ?");
+ st.setString(1, ";");
+ st.setString(2, ";");
+ st.setString(3, "$a$ $a$");
+
+ assertTrue(st.execute());
+ rs = st.getResultSet();
+ assertTrue(rs.next());
+ assertEquals(";", rs.getString(1));
+ assertFalse(rs.next());
+
+ assertTrue(st.getMoreResults());
+ rs = st.getResultSet();
+ assertTrue(rs.next());
+ assertEquals("$a$;$x $a$", rs.getString(1));
+ assertFalse(rs.next());
+
+ assertTrue(st.getMoreResults());
+ rs = st.getResultSet();
+ assertTrue(rs.next());
+ assertEquals("$a$ $a$", rs.getString(1));
+ assertFalse(rs.next());
+ st.close();
+ }
+
+ public void testComments() throws SQLException {
+ PreparedStatement st;
+ ResultSet rs;
+
+ st = conn.prepareStatement("SELECT /*?*/ /*/*/*/**/*/*/*/1;SELECT ?;--SELECT ?");
+ st.setString(1, "a");
+ assertTrue(st.execute());
+ assertTrue(st.getMoreResults());
+ assertFalse(st.getMoreResults());
+ st.close();
+
+ st = conn.prepareStatement("SELECT /**/'?'/*/**/*/ WHERE '?'=/*/*/*?*/*/*/--?\n?");
+ st.setString(1, "?");
+ rs = st.executeQuery();
+ assertTrue(rs.next());
+ assertEquals("?", rs.getString(1));
+ assertFalse(rs.next());
+ st.close();
+ }
+
public void testDouble() throws SQLException
{
PreparedStatement pstmt = conn.prepareStatement("CREATE TEMP TABLE double_tab (max_double float, min_double
float,null_value float)");
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/StatementTest.java
pgjdbc.323e7d139a7b/org/postgresql/test/jdbc2/StatementTest.java
*** pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/StatementTest.java 2006-11-01 22:41:41.000000000 +0100
--- pgjdbc.323e7d139a7b/org/postgresql/test/jdbc2/StatementTest.java 2006-11-01 22:41:41.000000000 +0100
***************
*** 390,395 ****
--- 390,442 ----
assertTrue(!rs.next());
}
+ public void testParsingDollarQuotes() throws SQLException
+ {
+ // dollar-quotes are supported in the backend since version 8.0
+ if (!TestUtil.haveMinimumServerVersion(con, "8.0"))
+ return;
+
+ Statement st = con.createStatement();
+ ResultSet rs;
+
+ rs = st.executeQuery("SELECT '$a$ ; $a$'");
+ assertTrue(rs.next());
+ assertEquals("$a$ ; $a$", rs.getObject(1));
+ rs.close();
+
+ rs = st.executeQuery("SELECT $$;$$");
+ assertTrue(rs.next());
+ assertEquals(";", rs.getObject(1));
+ rs.close();
+
+ rs = st.executeQuery("SELECT $OR$$a$'$b$a$$OR$ WHERE '$a$''$b$a$'=$OR$$a$'$b$a$$OR$OR ';'=''");
+ assertTrue(rs.next());
+ assertEquals("$a$'$b$a$", rs.getObject(1));
+ assertFalse(rs.next());
+ rs.close();
+
+ rs = st.executeQuery("SELECT $B$;$b$B$");
+ assertTrue(rs.next());
+ assertEquals(";$b", rs.getObject(1));
+ rs.close();
+
+ rs = st.executeQuery("SELECT $c$c$;$c$");
+ assertTrue(rs.next());
+ assertEquals("c$;", rs.getObject(1));
+ rs.close();
+
+ rs = st.executeQuery("SELECT $A0$;$A0$ WHERE ''=$t$t$t$ OR ';$t$'=';$t$'");
+ assertTrue(rs.next());
+ assertEquals(";", rs.getObject(1));
+ assertFalse(rs.next());
+ rs.close();
+
+ st.executeQuery("SELECT /* */$$;$$/**//*;*/").close();
+ st.executeQuery("SELECT /* */--;\n$$a$$/**/--\n--;\n").close();
+
+ st.close();
+ }
+
public void testUnbalancedParensParseError() throws SQLException
{
Statement stmt = con.createStatement();
pgsql-jdbc by date: