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 451FC6CD.6060904@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:
> Dave Cramer wrote:
>  >> I guess it's just a matter of coding, right? Would the JDBC
>  >> maintainers accept a patch for the 8.2 release of the driver?
>  > Absolutely!
>
> Ok, I am going to write support for this... [snip]

Here is a first version of the patch. It works well for me so far, so I
am asking for comments now.

This patch only adds knowledge about dollar-quotes to
v3.QueryExecutorImpl.parseQuery() for now, but there are other possible
candidates:
- V2Query: for people connecting to postgres >= 8.0 using
   ?protocolVersion=2
- AbstractJdbc2Statement.modifyJdbcCall() for prepareCall: I would not
   expect someone to use dollar-quoting here, but single-quotes are
   supported, so adding dollar-quoting does not seem completely off
   track.

I have tried to make the patch rather non-invasive. I thought about
improving parseQuery to be more efficient, but without having done any
profiling, I did not want to considerably change the existing code
paths. All JUnit tests still pass running against an 8.1 server.

p1-remove-unused-var.patch:

org/postgresql/core/v3/QueryExecutorImpl.java:
- Remove an unused variable from parseQuery

p2-v3protocol-dolquot.patch:

org/postgresql/core/Utils.java:
- Add static methods isDollarQuoteStartChar(char) and
   isDollarQuoteContChar(char)

org/postgresql/core/v3/QueryExecutorImpl.java:
- Add support for dollar-quoting in parseQuery

org/postgresql/test/jdbc3/Jdbc3DollarQuotingTest.java:
- New file with JUnit tests for new code

org/postgresql/test/jdbc3/Jdbc3TestSuite.java:
- Add the new JUnit TestCase to the test suite if server >= 8.0 and
   protocolVersion == 3

Best Regards
Michael Paesold
diff -cr pgjdbc-orig/org/postgresql/core/v3/QueryExecutorImpl.java pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java
*** pgjdbc-orig/org/postgresql/core/v3/QueryExecutorImpl.java    Sat Sep 30 19:45:14 2006
--- pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java    Sat Sep 30 19:37:41 2006
***************
*** 63,69 ****
          ArrayList statementList = new ArrayList();
          ArrayList fragmentList = new ArrayList(15);

-         boolean inQuotes = false;
          int fragmentStart = 0;

          boolean inSingleQuotes = false;
--- 63,68 ----
diff -crN pgjdbc-orig/org/postgresql/core/Utils.java pgjdbc/org/postgresql/core/Utils.java
*** pgjdbc-orig/org/postgresql/core/Utils.java    Tue Jan 11 09:25:43 2005
--- pgjdbc/org/postgresql/core/Utils.java    Sun Oct  1 14:59:57 2006
***************
*** 53,56 ****
--- 53,81 ----
              throw new RuntimeException("Unexpected exception: UTF-8 charset not supported: " + e);
          }
      }
+
+     /**
+      * 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) {
+         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');
+     }
  }
diff -crN pgjdbc-orig/org/postgresql/core/v3/QueryExecutorImpl.java
pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java
*** pgjdbc-orig/org/postgresql/core/v3/QueryExecutorImpl.java    Sat Sep 30 21:30:26 2006
--- pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java    Sun Oct  1 14:56:18 2006
***************
*** 67,72 ****
--- 67,74 ----

          boolean inSingleQuotes = false;
          boolean inDoubleQuotes = false;
+         boolean inDollarQuotes = false;
+         String dollarQuoteTag = null;
          int inParen = 0;

          char []aChars = query.toCharArray();
***************
*** 83,97 ****
                  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;
--- 85,99 ----
                  break;

              case '\'':
!                 inSingleQuotes = !inDoubleQuotes && !inDollarQuotes && !inSingleQuotes;
                  break;

              case '"':
!                 inDoubleQuotes = !inSingleQuotes && !inDollarQuotes && !inDoubleQuotes;
                  break;

              case '?':
!                 if (withParameters && !inSingleQuotes && !inDoubleQuotes && !inDollarQuotes)
                  {
                      fragmentList.add(query.substring(fragmentStart, i));
                      fragmentStart = i + 1;
***************
*** 99,115 ****
                  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;
--- 101,164 ----
                  break;

              case '(':
!                 if (!inSingleQuotes && !inDoubleQuotes && !inDollarQuotes)
                          inParen++;
                  break;

              case ')':
!                 if (!inSingleQuotes && !inDoubleQuotes && !inDollarQuotes)
                          inParen--;
                  break;

+             case '$':
+                 if (!inSingleQuotes && !inDoubleQuotes) {
+                     if (!inDollarQuotes)
+                     {
+                         // possible dollar quote start
+                         if (i + 1 < aChars.length)
+                         {
+                             int endIdx = -1;
+                             if (aChars[i + 1] == '$')
+                                 endIdx = i + 1;
+                             else if (Utils.isDollarQuoteStartChar(aChars[i + 1]))
+                             {
+                                 for (int d = i + 2; d < aChars.length; ++d)
+                                 {
+                                     if (aChars[d] == '$')
+                                     {
+                                         endIdx = d;
+                                         break;
+                                     }
+                                     else if (!Utils.isDollarQuoteContChar(aChars[d]))
+                                         break;
+                                 }
+                             }
+                             if (endIdx > 0)
+                             {
+                                 inDollarQuotes = true;
+                                 // dollarQuoteTag includes start and end $ character
+                                 dollarQuoteTag = query.substring(i, endIdx + 1);
+                                 i = endIdx; // main loop continues at endIdx + 1
+                             }
+                         }
+                     }
+                     else
+                     {
+                         // possible dollar quote end
+                         if (i + dollarQuoteTag.length() <= aChars.length &&
+                                 query.substring(i, i + dollarQuoteTag.length()).equals(
+                                         dollarQuoteTag))
+                         {
+                             inDollarQuotes = false;
+                             i += dollarQuoteTag.length() - 1;
+                             dollarQuoteTag = null;
+                         }
+                     }
+                 }
+                 break;
+
              case ';':
!                 if (!inSingleQuotes && !inDoubleQuotes && !inDollarQuotes && inParen == 0)
                  {
                      fragmentList.add(query.substring(fragmentStart, i));
                      fragmentStart = i + 1;
diff -crN pgjdbc-orig/org/postgresql/test/jdbc3/Jdbc3DollarQuotingTest.java
pgjdbc/org/postgresql/test/jdbc3/Jdbc3DollarQuotingTest.java
*** pgjdbc-orig/org/postgresql/test/jdbc3/Jdbc3DollarQuotingTest.java    Thu Jan  1 01:00:00 1970
--- pgjdbc/org/postgresql/test/jdbc3/Jdbc3DollarQuotingTest.java    Sun Oct  1 14:46:54 2006
***************
*** 0 ****
--- 1,139 ----
+ /*-------------------------------------------------------------------------
+  *
+  * Copyright (c) 2006, PostgreSQL Global Development Group
+  *
+  * IDENTIFICATION
+  *   $PostgreSQL$
+  *
+  *-------------------------------------------------------------------------
+  */
+ package org.postgresql.test.jdbc3;
+
+ import org.postgresql.test.TestUtil;
+
+ import java.sql.*;
+
+ import junit.framework.TestCase;
+
+ /*
+  * Test for dollar-quoting
+  */
+ public class Jdbc3DollarQuotingTest extends TestCase {
+
+     private Connection con;
+
+     public Jdbc3DollarQuotingTest(String name) {
+         super(name);
+     }
+
+     protected void setUp() throws Exception
+     {
+         con = TestUtil.openDB();
+         TestUtil.createTable(con, "testdquot", "t text");
+         Statement stmt = con.createStatement();
+
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "';'"));
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "'?'"));
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "'$a$ $a$'"));
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "'$a$''$b$a$'"));
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "';$b'"));
+         stmt.executeUpdate(TestUtil.insertSQL("testdquot", "'c$;'"));
+
+         stmt.close();
+     }
+
+     protected void tearDown() throws Exception
+     {
+         TestUtil.dropTable(con, "testdquot");
+         TestUtil.closeDB(con);
+     }
+
+     public void testSimple() throws SQLException
+     {
+         Statement st = con.createStatement();
+         ResultSet rs;
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t='$a$ $a$'");
+         assertTrue(rs.next());
+         assertEquals("$a$ $a$", rs.getObject(1));
+         rs.close();
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=$$;$$");
+         assertTrue(rs.next());
+         assertEquals(";", rs.getObject(1));
+         rs.close();
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=$OR$$a$'$b$a$$OR$OR t=';;'");
+         assertTrue(rs.next());
+         assertEquals("$a$'$b$a$", rs.getObject(1));
+         assertFalse(rs.next());
+         rs.close();
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=$B$;$b$B$");
+         assertTrue(rs.next());
+         assertEquals(";$b", rs.getObject(1));
+         rs.close();
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=$c$c$;$c$");
+         assertTrue(rs.next());
+         assertEquals("c$;", rs.getObject(1));
+         rs.close();
+
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=$t$t$t$ OR t=';'");
+         assertTrue(rs.next());
+         assertEquals(";", rs.getObject(1));
+         assertFalse(rs.next());
+         rs.close();
+
+         st.executeUpdate("UPDATE testdquot SET t=';;' WHERE t=$q0$;$q0$;"
+                 + "UPDATE testdquot SET t='' WHERE t=';;'");
+         rs = st.executeQuery("SELECT t FROM testdquot WHERE t=''");
+         assertTrue(rs.next());
+         assertEquals("", rs.getObject(1));
+         assertFalse(rs.next());
+         rs.close();
+
+         st.close();
+     }
+
+     public void testParameterized() throws SQLException
+     {
+         PreparedStatement st;
+         ResultSet rs;
+
+         st = con.prepareStatement("SELECT t FROM testdquot WHERE t=$$?$$ AND t=?");
+         st.setString(1, "?");
+         rs = st.executeQuery();
+         assertTrue(rs.next());
+         assertEquals("?", rs.getString(1));
+         assertFalse(rs.next());
+         st.close();
+
+         st = con.prepareStatement("SELECT t FROM testdquot WHERE t=$q_1$'$q_1$ OR t=?;"
+                 + "SELECT t FROM testdquot WHERE t=? OR t=$c$c$;$c$;"
+                 + "SELECT t FROM testdquot WHERE t=?");
+         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());
+         assertTrue(rs.next());
+         assertFalse(rs.next());
+
+         assertTrue(st.getMoreResults());
+         rs = st.getResultSet();
+         assertTrue(rs.next());
+         assertEquals("$a$ $a$", rs.getString(1));
+         assertFalse(rs.next());
+
+     }
+
+ }
diff -crN pgjdbc-orig/org/postgresql/test/jdbc3/Jdbc3TestSuite.java
pgjdbc/org/postgresql/test/jdbc3/Jdbc3TestSuite.java
*** pgjdbc-orig/org/postgresql/test/jdbc3/Jdbc3TestSuite.java    Mon May 15 11:35:57 2006
--- pgjdbc/org/postgresql/test/jdbc3/Jdbc3TestSuite.java    Sun Oct  1 15:17:30 2006
***************
*** 34,39 ****
--- 34,43 ----
              {
                  suite.addTestSuite(Jdbc3CallableStatementTest.class);
              }
+             if ( TestUtil.haveMinimumServerVersion(con, "8.0") && TestUtil.isProtocolVersion(con, 3))
+             {
+                 suite.addTestSuite(Jdbc3DollarQuotingTest.class);
+             }
              con.close();
          }
          catch (Exception ex )

pgsql-jdbc by date:

Previous
From: Michael Paesold
Date:
Subject: Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails
Next
From: Dave Cramer
Date:
Subject: Re: Query ResultSet parsing speedup patch (resend)