Обсуждение: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails to execute (SimpleQuery splitting invalid)

Поиск
Список
Период
Сортировка

[pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails to execute (SimpleQuery splitting invalid)

От
Grégory Chazalon
Дата:
Hi,

I've encountered a strange behavior of the JDBC driver 8.1.407 with PostgreSQL 8.1.4 (windows platform). I really
suspectthis is a bug inside the driver implementation, that's reason why I write this post.  

Here is the use case :

I want to create a trigger with the JDBC API. This trigger function uses dollar-quoted escaped string literal. The
reasonfor this is that I use a search_path variable for the connected user, and I want postgres to automatically add
thecorrect db schema inside my trigger declaration. Here it is : 

CREATE OR REPLACE FUNCTION procedure_insert_deleted_document() RETURNS TRIGGER AS $trigger_insert_deleted_document$
BEGIN INSERT INTO DELETED_DOCUMENT (DOC_ID, modificationDate, DOCVAULT_ID, guid) VALUES(OLD.DOC_ID, localtimestamp,
OLD.DOCVAULT_ID,OLD.guid); RETURN OLD; END; $trigger_insert_deleted_document$ LANGUAGE plpgsql;  
CREATE TRIGGER trigger_insert_deleted_document AFTER DELETE ON gch.DOCUMENT FOR EACH ROW EXECUTE PROCEDURE
procedure_insert_deleted_document();

The problem is that this statement is internally broke-up into too many SimpleQuery objects by the driver. In fact, the
$sign escape doesn't seem to be recognize and the above statement is splitted up in five (after each semicolon):  

CREATE OR REPLACE FUNCTION procedure_insert_deleted_document() RETURNS TRIGGER AS $trigger_insert_deleted_document$
BEGIN INSERT INTO DELETED_DOCUMENT (DOC_ID, modificationDate, DOCVAULT_ID, guid) VALUES(OLD.DOC_ID, localtimestamp,
OLD.DOCVAULT_ID,OLD.guid) 

RETURN OLD

END

$trigger_insert_deleted_document$ LANGUAGE plpgsql

CREATE TRIGGER trigger_insert_deleted_document AFTER DELETE ON gch.DOCUMENT FOR EACH ROW EXECUTE PROCEDURE
procedure_insert_deleted_document()


Of course, the execution of these statement fails after the first one with the following error :

ERROR: unterminated dollar-quoted string at or near "$trigger_insert_deleted_document$ BEGIN INSERT INTO
gch.DELETED_DOCUMENT(DOC_ID, modificationDate, DOCVAULT_ID, guid) VALUES(OLD.DOC_ID, localtimestamp, OLD.DOCVAULT_ID,
OLD.guid)"


On the contrary, if I fall back to standard quoted string, the statement below is this time split in two SimpleQuery
andsucceeds. 

CREATE OR REPLACE FUNCTION procedure_insert_deleted_document() RETURNS TRIGGER AS ' BEGIN INSERT INTO
gch.DELETED_DOCUMENT(DOC_ID, modificationDate, DOCVAULT_ID, guid) VALUES(OLD.DOC_ID, localtimestamp, OLD.DOCVAULT_ID,
OLD.guid);RETURN OLD; END; ' LANGUAGE plpgsql;  
CREATE TRIGGER trigger_insert_deleted_document AFTER DELETE ON gch.DOCUMENT FOR EACH ROW EXECUTE PROCEDURE
procedure_insert_deleted_document();

But this workaround doesn't suit me as it requires explicit schema prefix inside the trigger.


So my question is : Does this is a known issue of the JDBC driver or does it remind you something equivalent ?
(I can provide further details if needed)

Thanks for your answer

Regards,

Gregory




=?iso-8859-1?Q?Gr=E9gory_Chazalon?= <Gregory.Chazalon@advestigo.com> writes:
> I've encountered a strange behavior of the JDBC driver 8.1.407 with PostgreSQL 8.1.4 (windows platform). I really
suspectthis is a bug inside the driver implementation, that's reason why I write this post.  

Last I heard, the JDBC driver hadn't been taught about dollar quoting at
all, so it's not surprising that a DQ literal within a query would
confuse it.

This ought to get fixed sometime ...

            regards, tom lane

Re: [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails to execute (SimpleQuery splitting invalid)

От
Grégory Chazalon
Дата:
Thanks for your answer Tom.

It's too bad that the driver doesn't support this...
I have no idea how difficult would it be to include such an improvement.
However, do you think there is a chance to see this in one of the next releases ?

Needless to say it would be greatly appreciated, at least by one user :-)

Regards,

Gregory


-----Message d'origine-----
De : Tom Lane [mailto:tgl@sss.pgh.pa.us]
Envoyé : mardi 22 août 2006 15:10
À : Grégory Chazalon
Cc : pgsql-jdbc@postgresql.org
Objet : Re: [JDBC] [pgsql-jdbc] dollar-quoted CREATE FUNCTION statement fails to execute (SimpleQuery splitting
invalid) 

=?iso-8859-1?Q?Gr=E9gory_Chazalon?= <Gregory.Chazalon@advestigo.com> writes:
> I've encountered a strange behavior of the JDBC driver 8.1.407 with PostgreSQL 8.1.4 (windows platform). I really
suspectthis is a bug inside the driver implementation, that's reason why I write this post.  

Last I heard, the JDBC driver hadn't been taught about dollar quoting at all, so it's not surprising that a DQ literal
withina query would confuse it. 

This ought to get fixed sometime ...

            regards, tom lane

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

От
Michael Paesold
Дата:
About dollar-quoting in the JDBC driver...

Tom Lane wrote:
 > Last I heard, the JDBC driver hadn't been taught about dollar quoting at
 > all, so it's not surprising that a DQ literal within a query would
 > confuse it.
 >
 > This ought to get fixed sometime ...

Grégory Chazalon write:
> It's too bad that the driver doesn't support this... I have no idea how
> difficult would it be to include such an improvement. However, do you
> think there is a chance to see this in one of the next releases ?
>
> Needless to say it would be greatly appreciated, at least by one user
> :-)

Me too.

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?

Best Regards
Michael Paesold

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

От
Dave Cramer
Дата:
On 28-Sep-06, at 7:13 AM, Michael Paesold wrote:

> About dollar-quoting in the JDBC driver...
>
> Tom Lane wrote:
> > Last I heard, the JDBC driver hadn't been taught about dollar
> quoting at
> > all, so it's not surprising that a DQ literal within a query would
> > confuse it.
> >
> > This ought to get fixed sometime ...
>
> Grégory Chazalon write:
>> It's too bad that the driver doesn't support this... I have no
>> idea how
>> difficult would it be to include such an improvement. However, do you
>> think there is a chance to see this in one of the next releases ?
>> Needless to say it would be greatly appreciated, at least by one user
>> :-)
>
> Me too.
>
> 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!
>
> Best Regards
> Michael Paesold
>
> ---------------------------(end of
> broadcast)---------------------------
> TIP 9: In versions below 8.0, the planner will ignore your desire to
>       choose an index scan if your joining column's datatypes do not
>       match
>


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

От
Michael Paesold
Дата:
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, but I need some help with
determining if a character is valid for a dollar-quote tag. I am
currently looking at the original patch implementing dollar-quoting in
the backend parser, see here:
http://developer.postgresql.org/cvsweb.cgi/pgsql/src/backend/parser/scan.l.diff?r1=1.114;r2=1.115

scan.l defines:
dolq_start        [A-Za-z\200-\377_]
dolq_cont        [A-Za-z\200-\377_0-9]

Some questions here:
- What are the \200-\377 characters?

- Any ideas how to efficiently check this in Java?
There is Character.isLetter() etc., but this includes all Unicode
letters, not only letters valid as identifiers in the backend.

Otherwise I can still use something like
public static boolean isDollarQuoteStartChar(char c) {
    return (c >= 'A' && c <= 'Z') || ...
}

I am currently working on dollar-quote support in
v3.QueryExecutorImpl.parseQuery(..), but I also want to add it to
V2Query. Where should I put such a static method like
isDollarQuoteStartChar? org.postgresql.core.Utils?

Best Regards
Michael Paesold


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

От
Tom Lane
Дата:
Michael Paesold <mpaesold@gmx.at> writes:
> scan.l defines:
> dolq_start        [A-Za-z\200-\377_]
> dolq_cont        [A-Za-z\200-\377_0-9]

> Some questions here:
> - What are the \200-\377 characters?

Basically, that's going to cover any non-7-bit-ASCII character
(including multibyte characters).  I'm not sure if Java has an
equivalent of ctype.h's isascii() but that'd probably be what
you want to use.  Checking if the Unicode code point is > 127
would work too, if Java lets you do that.

            regards, tom lane

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

От
Michael Paesold
Дата:
Tom Lane schrieb:
 > Michael Paesold <mpaesold@gmx.at> writes:
 >> scan.l defines:
 >> dolq_start        [A-Za-z\200-\377_]
 >> dolq_cont        [A-Za-z\200-\377_0-9]
 >
 >> Some questions here:
 >> - What are the \200-\377 characters?
 >
 > Basically, that's going to cover any non-7-bit-ASCII character
 > (including multibyte characters).  I'm not sure if Java has an
 > equivalent of ctype.h's isascii() but that'd probably be what
 > you want to use.  Checking if the Unicode code point is > 127
 > would work too, if Java lets you do that.

Ok, CharUtils of commons-lang of Apache.org defines isAscii as simply as:

public static boolean isAscii(char ch) {
         return (ch < 128);
}

So something like this should do the trick:

public static boolean isDollarQuoteStartChar(char c) {
     return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
             || (c == '_') || (c > 127);
}

(c > 127) should be the same as (c >= '\200'), but I find the first one
more readable. I am probably not used to reading hex numbers. ;-)

Thanks for your help.

Best Regards
Michael Paesold


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

От
Markus Schaber
Дата:
Hi, Michael,

Michael Paesold wrote:

> (c > 127) should be the same as (c >= '\200'), but I find the first one
> more readable. I am probably not used to reading hex numbers. ;-)

Just to be picky: The "200" is octal, not hex. The hex value for 128 is
0x80. :-)


HTH,
Markus

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

От
Michael Paesold
Дата:
Markus Schaber wrote:
> Michael Paesold wrote:
>
>> (c > 127) should be the same as (c >= '\200'), but I find the first one
>> more readable. I am probably not used to reading hex numbers. ;-)
>
> Just to be picky: The "200" is octal, not hex. The hex value for 128 is
> 0x80. :-)

You are right, of course. It caught my eye after sending the mail, but I
hoped nobody would notice. People are just to bright here. :-)

Best Regards
Michael Paesold


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

От
Michael Paesold
Дата:
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 )

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

От
Michael Paesold
Дата:
Michael Paesold wrote:
 > Here is a first version of the patch. It works well for me so far, so
 > I am asking for comments now.

Before everyone forgets about my patch in the discussion about the later
patch by Jure Koren...

I have already spent some hours on doing research, coding, creating the
test case. So I am willing to improve my work, of course. It just much
easier with input from the community. I would really welcome any
suggestions or criticism.

While at it, don't you think that we should handle SQL comments in
parseQuery? For example when loading a schema from a file using JDBC,
there might be comments in it that will break the current code. Does the
JDBC spec say anything about that matter?

One other thing that I wanted to try is to not parse the whole query in
a single loop, but instead split up the parsing a bit more, in the style of:

case '"': scan for next '"';
case '\'': scan for matching '\'';
case '$': if dollar-quote, scan for end of dollar-quote

It would simplify the other branches of the switch statement and we
could get rid of the if (!in... && !in...) stuff, then.

Do you think that would be an improvement?

Best Regards
Michael Paesold






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

От
Kris Jurka
Дата:

On Tue, 3 Oct 2006, Michael Paesold wrote:

> While at it, don't you think that we should handle SQL comments in
> parseQuery? For example when loading a schema from a file using JDBC, there
> might be comments in it that will break the current code. Does the JDBC spec
> say anything about that matter?

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.

Another parsing problem that we'll have with the 8.2 release is the
standard_conforming_strings configuration variable.  I would appreciate
anyone taking a look at that and the issues that it will create, not just
for query parsing, but also for escaping in the V2 path and
DatabaseMetaData methods.

Kris Jurka

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

От
Michael Paesold
Дата:
Kris Jurka wrote:
 > Not parsing comments, both -- and /* */ is a complaint we here as
 > often if not if not with more frequency than dollar quoting.

Ok, I will update my dollar-quoting patch to handle comments, too. I am
going to update V2Query as well, then. Anything else that is missing
from the query parsing right now?

 > Another parsing problem that we'll have with the 8.2 release is the
 > standard_conforming_strings configuration variable.  I would appreciate
 > anyone taking a look at that and the issues that it will create, not
 > just for query parsing, but also for escaping in the V2 path and
 > DatabaseMetaData methods.

I was just going to open a new thread on this. :-) As I was reading
through the code regarding dollar-quoting, I became aware of the issue.
So yes, I am willing to help.

Best Regards
Michael Paesold


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

От
Michael Paesold
Дата:
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");

    }
}

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

От
Kris Jurka
Дата:

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?


> - 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?
>

Sharing would be good, but I agree there's no easy way for that to
work.  Copying should be OK.

Kris Jurka


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

От
Michael Paesold
Дата:
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.

> Sharing would be good, but I agree there's no easy way for that to
> work.  Copying should be OK.

We will see shortly, if it works. I am going to post a new patch in the
next two days. Soon enough?

Best Regards
Michael Paesold

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

От
Michael Paesold
Дата:
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();

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

От
Dave Cramer
Дата:
Michael,

I'm only looking at this from a performance standpoint.

1) testing for array.length in a loop is fairly expensive compared to
testing for 0, not a big deal but it adds up.
2) I'm wondering what the cost of a switch is for two cases
( candidly I don't know the answer to this,and it's quite likely that
modern compilers will turn it into an if else anyway.)

Dave

On 1-Nov-06, at 4:47 PM, Michael Paesold wrote:

> 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();


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

От
Michael Paesold
Дата:
Hi Dave,

first, I forgot to say that I did do performance tests using the
previously posted test case. The new version is definitely faster than
the original code, although only marginally. When doing my first version
of the patch, I tested different code constructs for speed. I chose the
one with the best performance.

I ran the tests with both 1.4.2 and 1.5.0. It might be possible that the
performance will be worse in an earlier versions of the JDK with a less
sophisticated hot-spot compiler.

Dave Cramer wrote:
> Michael,
>
> I'm only looking at this from a performance standpoint.
>
> 1) testing for array.length in a loop is fairly expensive compared to
> testing for 0, not a big deal but it adds up.

Could you give me a hint what part of the code you were looking at? And
what you would want me to change? Do you think we should scan the query
backwards (testing the offset against zero -- not sure if this is a good
idea) or do you want to make the query char[] zero-terminated so that we
can test against '\0'?

Btw. changing the code to
int len = array.length;
for (....; i < len ; ...)
did not give any performance advantage for me. I think the compiler can
quite effectively optimize access to .length here.


> 2) I'm wondering what the cost of a switch is for two cases ( candidly I
> don't know the answer to this,and it's quite likely that modern
> compilers will turn it into an if else anyway.)

Hmm, perhaps it's bad coding style that I left out the empty default.
But I only see this example in the code (2 cases + an implicit default):

+             switch (query[offset])
+             {
+             case '\\':
+                 ++offset;
+                 break;
+             case '\'':
+                 return offset;
+             }

That would really translate to:
if (query[offset] = '\\') ... else if (query[offset] = '\'') ... /* else
  nothing */

Should I add an empty default in that code sections? When I originally
wrote that code, it was all in that main loop, so I wanted to keep it
short. Now that it is split into several methods, I see no reason for
brevity.

Best Regards,
Michael Paesold





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

От
Dave Cramer
Дата:
On 1-Nov-06, at 8:19 PM, Michael Paesold wrote:

> Hi Dave,
>
> first, I forgot to say that I did do performance tests using the
> previously posted test case. The new version is definitely faster
> than the original code, although only marginally. When doing my
> first version of the patch, I tested different code constructs for
> speed. I chose the one with the best performance.
>
> I ran the tests with both 1.4.2 and 1.5.0. It might be possible
> that the performance will be worse in an earlier versions of the
> JDK with a less sophisticated hot-spot compiler.

Cool my comments were pretty minor anyway.
>
> Dave Cramer wrote:
>> Michael,
>> I'm only looking at this from a performance standpoint.
>> 1) testing for array.length in a loop is fairly expensive compared
>> to testing for 0, not a big deal but it adds up.
>
> Could you give me a hint what part of the code you were looking at?
> And what you would want me to change? Do you think we should scan
> the query backwards (testing the offset against zero -- not sure if
> this is a good idea) or do you want to make the query char[] zero-
> terminated so that we can test against '\0'?
>
> Btw. changing the code to
> int len = array.length;
> for (....; i < len ; ...)
> did not give any performance advantage for me. I think the compiler
> can quite effectively optimize access to .length here.
>
>
>> 2) I'm wondering what the cost of a switch is for two cases
>> ( candidly I don't know the answer to this,and it's quite likely
>> that modern compilers will turn it into an if else anyway.)
>
> Hmm, perhaps it's bad coding style that I left out the empty
> default. But I only see this example in the code (2 cases + an
> implicit default):
>
> +             switch (query[offset])
> +             {
> +             case '\\':
> +                 ++offset;
> +                 break;
> +             case '\'':
> +                 return offset;
> +             }
>
> That would really translate to:
> if (query[offset] = '\\') ... else if (query[offset] = '\'') ... /*
> else  nothing */
>
> Should I add an empty default in that code sections? When I
> originally wrote that code, it was all in that main loop, so I
> wanted to keep it short. Now that it is split into several methods,
> I see no reason for brevity.
>
> Best Regards,
> Michael Paesold
>
>
>
>
>


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

От
Michael Paesold
Дата:
Dave Cramer wrote:
> On 1-Nov-06, at 8:19 PM, Michael Paesold wrote:
>
>> Hi Dave,
>>
>> first, I forgot to say that I did do performance tests using the
>> previously posted test case. The new version is definitely faster than
>> the original code, although only marginally. When doing my first
>> version of the patch, I tested different code constructs for speed. I
>> chose the one with the best performance.
>>
>> I ran the tests with both 1.4.2 and 1.5.0. It might be possible that
>> the performance will be worse in an earlier versions of the JDK with a
>> less sophisticated hot-spot compiler.
>
> Cool my comments were pretty minor anyway.

Nevertheless, attached is an updated patch that adds the two missing
"default: break;" cases to the switch statements for better readability.

Best Regards
Michael Paesold
diff -crN pgjdbc.862ca68b9ea5/org/postgresql/core/Parser.java pgjdbc.0af947a7aad5/org/postgresql/core/Parser.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/Parser.java    1970-01-01 01:00:00.000000000 +0100
--- pgjdbc.0af947a7aad5/org/postgresql/core/Parser.java    2006-11-02 09:08:26.000000000 +0100
***************
*** 0 ****
--- 1,215 ----
+ /*-------------------------------------------------------------------------
+ *
+ * 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.
+      *
+      * Note: for <tt>'single '' quote in string'</tt>, this method currently
+      * returns the offset of first <tt>'</tt> character after the initial
+      * one. The caller must call the method a second time for the second
+      * part of the quoted string.
+      */
+     public static int parseSingleQuotes(final char[] query, int offset) {
+         while (++offset < query.length)
+         {
+             switch (query[offset])
+             {
+             case '\\':
+                 ++offset;
+                 break;
+             case '\'':
+                 return offset;
+             default:
+                 break;
+             }
+         }
+         return query.length;
+     }
+
+     /**
+      * Find the end of the double-quoted string starting at the given offset.
+      *
+      * Note: for <tt>"double "" quote in string"</tt>,
+      * this method currently returns the offset of first <tt>"</tt>
+      * character after the initial one. The caller must call the method a
+      * second time for the second part of the quoted string.
+      */
+     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;
+                 default:
+                     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.0af947a7aad5/org/postgresql/core/v2/V2Query.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/v2/V2Query.java    2006-11-02 09:08:26.000000000 +0100
--- pgjdbc.0af947a7aad5/org/postgresql/core/v2/V2Query.java    2006-11-02 09:08:26.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.0af947a7aad5/org/postgresql/core/v3/QueryExecutorImpl.java
*** pgjdbc.862ca68b9ea5/org/postgresql/core/v3/QueryExecutorImpl.java    2006-11-02 09:08:26.000000000 +0100
--- pgjdbc.0af947a7aad5/org/postgresql/core/v3/QueryExecutorImpl.java    2006-11-02 09:08:26.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.0af947a7aad5/org/postgresql/test/jdbc2/PreparedStatementTest.java
*** pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/PreparedStatementTest.java    2006-11-02 09:08:26.000000000 +0100
--- pgjdbc.0af947a7aad5/org/postgresql/test/jdbc2/PreparedStatementTest.java    2006-11-02 09:08:26.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.0af947a7aad5/org/postgresql/test/jdbc2/StatementTest.java
*** pgjdbc.862ca68b9ea5/org/postgresql/test/jdbc2/StatementTest.java    2006-11-02 09:08:26.000000000 +0100
--- pgjdbc.0af947a7aad5/org/postgresql/test/jdbc2/StatementTest.java    2006-11-02 09:08:26.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();

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

От
Kris Jurka
Дата:

On Thu, 2 Nov 2006, Michael Paesold wrote:

> Nevertheless, attached is an updated patch that adds the two missing
> "default: break;" cases to the switch statements for better readability.
>

Applied.  Thanks.

Kris Jurka