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:

Previous
From: "ludovic orban"
Date:
Subject: Re: XA end then join fix for WebLogic
Next
From: Dave Cramer
Date:
Subject: Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails