Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails

Поиск
Список
Период
Сортировка
От Michael Paesold
Тема Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails
Дата
Msg-id 4526085F.5030604@gmx.at
обсуждение исходный текст
Ответ на Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement  (Kris Jurka <books@ejurka.com>)
Ответы Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement
Список pgsql-jdbc
Kris Jurka wrote:
> I haven't looked at either dollar quote patch, but will hopefully get to
> that and other JDBC items later this week.  Not parsing comments, both
> -- and /* */ is a complaint we here as often if not if not with more
> frequency than dollar quoting.  (Apparently there's a Hibernate option
> that puts things in comments.)  So, yes that would be a good thing to
> look at.

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.

It passes all tests, including the newly created one. In addition to
supporting comments now, I have changed how we iterate over the
character array. Instead of having just one big loop parsing all
constructs, I have created tight loops for finding the end of certain
constructs, exactly for single, double, and dollar quotes, and comments.

This reduces the runtime with all new added "features" by ~10% in my
test case (see below), whereas my first implementation (as well as the
one from Jure Koren) had a worsening effect on runtime. Although the
code is longer because of the inner loops, I think it is not less
understandable, especially with all the new cases, which would add quite
much of if (!... && ! ...) otherwise.

I did the runtime tests using the attached ParserSpeedTest.java. It
reads a bunch of SQL files into memory and then runs the parser on them.
Example:
java ParserSpeedTest 10000 ../../app/schema/*.sql
(call parser 10000 times on all given *.sql files)

My todo list for this patch:
- Add the same capabilities to V2Query. Should I just copy the
corresponding parts of the code there? I don't see an easy way for code
sharing here. Although it would be possible to factor parts into extra
methods, I don't see that this would be a real win. Should I try? Comments?

- Move the tests from the new class to the places where they belong,
e.g. a testDollarQuotes method in the PreparedStatementTest class.

Comments?

Best Regards
Michael
Index: core/Utils.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/Utils.java,v
retrieving revision 1.4
diff -c -r1.4 Utils.java
*** core/Utils.java    11 Jan 2005 08:25:43 -0000    1.4
--- core/Utils.java    6 Oct 2006 07:37:45 -0000
***************
*** 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');
+     }
  }
Index: core/v3/QueryExecutorImpl.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java,v
retrieving revision 1.31
diff -c -r1.31 QueryExecutorImpl.java
*** core/v3/QueryExecutorImpl.java    7 Jul 2006 01:12:23 -0000    1.31
--- core/v3/QueryExecutorImpl.java    6 Oct 2006 07:37:46 -0000
***************
*** 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,195 ----
          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
!                 boolean done = false;
!                 while (!done && ++i < aChars.length)
!                 {
!                     switch (aChars[i])
!                     {
!                     case '\\':
!                         ++i;
!                         break;
!                     case '\'':
!                         done = true;
!                         break;
!                     }
!                 }
                  break;

!             case '"': // double-quotes
!                 while (++i < aChars.length && aChars[i] != '"') ;
                  break;

!             case '-': // -- style comments
!                 if (i + 1 < aChars.length && aChars[i + 1] == '-')
!                 {
!                     while (++i < aChars.length)
!                     {
!                         if (aChars[i] == '\r' || aChars[i] == '\n')
!                             break;
!                     }
!                 }
                  break;

!             case '/': // /* */ style comments
!                 if (i + 1 < aChars.length && aChars[i + 1] == '*')
                  {
!                     // /* /* */ */ nest, according to SQL spec
!                     int level = 1;
!                     for (i += 2; i < aChars.length; ++i)
!                     {
!                         switch (aChars[i-1]) {
!                         case '*':
!                             if (aChars[i] == '/')
!                             {
!                                 --level;
!                                 ++i; // don't parse / in */* twice
!                             }
!                             break;
!                         case '/':
!                             if (aChars[i] == '*')
!                             {
!                                 ++level;
!                                 ++i; // don't parse * in /*/ twice
!                             }
!                             break;
!                         }
!                         if (level == 0)
!                         {
!                             --i; // reset position to last '/' char
!                             break;
!                         }
!                     }
!                 }
!                 break;
!
!             case '$': // 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)
!                     {
!                         // found; note: tag includes start and end $ character
!                         String tag = query.substring(i, endIdx + 1);
!                         i = endIdx; // loop continues at endIdx + 1
!                         for (++i; i < aChars.length; ++i)
!                         {
!                             if (aChars[i] == '$' && i + tag.length() <= aChars.length &&
!                                 query.substring(i, i + tag.length()).equals(tag))
!                             {
!                                 i += tag.length() - 1;
!                                 break;
!                             }
!                         }
!                     }
                  }
                  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;
Index: test/jdbc3/Jdbc3TestSuite.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/test/jdbc3/Jdbc3TestSuite.java,v
retrieving revision 1.16
diff -c -r1.16 Jdbc3TestSuite.java
*** test/jdbc3/Jdbc3TestSuite.java    15 May 2006 09:35:57 -0000    1.16
--- test/jdbc3/Jdbc3TestSuite.java    6 Oct 2006 07:37:46 -0000
***************
*** 34,39 ****
--- 34,43 ----
              {
                  suite.addTestSuite(Jdbc3CallableStatementTest.class);
              }
+             if ( TestUtil.haveMinimumServerVersion(con, "8.0") && TestUtil.isProtocolVersion(con, 3))
+             {
+                 suite.addTestSuite(Jdbc3ParserTest.class);
+             }
              con.close();
          }
          catch (Exception ex )
Index: test/jdbc3/Jdbc3ParserTest.java
===================================================================
RCS file: test/jdbc3/Jdbc3ParserTest.java
diff -N test/jdbc3/Jdbc3ParserTest.java
*** /dev/null    1 Jan 1970 00:00:00 -0000
--- test/jdbc3/Jdbc3ParserTest.java    1 Jan 1970 00:00:00 -0000
***************
*** 0 ****
--- 1,152 ----
+ /*-------------------------------------------------------------------------
+  *
+  * 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 parser: quoting, dollar-quoting, comments, etc. in query
+  */
+ public class Jdbc3ParserTest extends TestCase {
+
+     private Connection con;
+
+     public Jdbc3ParserTest(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();
+
+         // we only check if the backend does not report an error here,
+         // e.g., because we split the query too often
+         st.executeQuery("SELECT /* */$$;$$/**/").close();
+         st.executeQuery("SELECT /* */--;\n$$a$$/**/--\n--;\n").close();
+         //SELECT /* */--;\n$$a$$/**/--\n--;\n
+
+         st.close();
+     }
+
+     public void testParameterized() throws SQLException
+     {
+         PreparedStatement st;
+         ResultSet rs;
+
+         st = con.prepareStatement("SELECT /*/*/*/**/*/*/*/1;SELECT ?;");
+         st.setString(1, "a");
+         assertTrue(st.execute());
+         assertTrue(st.getMoreResults());
+         assertFalse(st.getMoreResults());
+         st.close();
+
+         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());
+         st.close();
+     }
+
+ }
import org.postgresql.core.v3.QueryExecutorImpl;

import java.io.*;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

public class ParserSpeedTest {

    List queries;

    public ParserSpeedTest(List queries) {
        this.queries = queries;
    }

    public void run(int count)
    {
        QueryExecutorImpl q = new QueryExecutorImpl(null, null,
                new Properties(), null);
        for (int c = 0; c < count; ++c)
        {
            for (Iterator i = queries.iterator(); i.hasNext();)
            {
                String query = (String) i.next();
                q.createSimpleQuery(query);
            }
        }
    }

    public static void main(String[] args)
    {
        List queries = new LinkedList();

        if (args.length < 2)
        {
            System.out.println("Usage: java ParserSpeedTest count file.sql,...");
            System.exit(1);
        }

        int count = 1;

        try
        {
            count = Integer.parseInt(args[0]);

            for (int i = 1; i < args.length; i++)
            {
                String fname = args[i];
                File file = new File(fname);
                if (!file.exists() || !file.canRead())
                {
                    System.err.println("Argument \"" + fname
                            + "\" is not a valid file name");
                }

                StringWriter w = new StringWriter();
                Reader r = new FileReader(file);
                char[] buf = new char[2048];
                for (int read; (read = r.read(buf)) > 0;)
                {
                    w.write(buf, 0, read);
                }
                queries.add(w.getBuffer().toString());
            }
        } catch (IOException e)
        {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
            System.exit(1);
        }
        catch (NumberFormatException e)
        {
            System.err.println("First argument must be a number: " + args[0]);
            System.exit(1);
        }

        ParserSpeedTest test = new ParserSpeedTest(queries);
        long start = System.currentTimeMillis();
        test.run(count);
        long end = System.currentTimeMillis();
        long total = end - start;
        double time = ((double)total) / 1000;

        System.out.println("Files: " + queries.size() + "  Runs: " + count);
        System.out.println("Time: "
                + new BigDecimal(time).setScale(3, BigDecimal.ROUND_HALF_UP)
                + " s");

    }
}

В списке pgsql-jdbc по дате отправления:

Предыдущее
От: "Stefano B."
Дата:
Сообщение: CachedRowSetImpl: transaction isolation level error
Следующее
От: Kris Jurka
Дата:
Сообщение: Re: Query ResultSet parsing speedup patch (resend)