Thread: Bug with PreparedStatements using EXTRACT function

Bug with PreparedStatements using EXTRACT function

From
Matthias Böhm
Date:
Hi!

I found a bug in the postgresql jdbc driver (I'm using version 9.1 build
901). Constructing a prepared statement that uses the EXTRACT function in
the following way results in an PSQLException when trying to execute the
prepared statement:

***** CODE *****
PreparedStatement stmt2 = conn.prepareStatement(
        "SELECT EXTRACT (YEAR FROM DATE ?)");

java.sql.Date date = new java.sql.Date(
        new GregorianCalendar(1990, 5, 7).getTime().getTime());

stmt2.setDate(1, date);

// here I get an SQL exception:
res = stmt2.executeQuery();
***** END CODE *****

The exception thrown is the following (in English the German "Syntaxfehler
bei »$1«" is "syntax error at $1"):

Exception in thread "main" org.postgresql.util.PSQLException: FEHLER:
Syntaxfehler bei »$1«
  Position: 31
    at
org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorI
mpl.java:2102)
    at
org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.ja
va:1835)
    at
org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.j
ava:500)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2St
atement.java:388)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.executeQuery(AbstractJdbc2Statem
ent.java:273)
    at pitas.shipdbs.storage.Bug.main(Bug.java:42)

Executing a simple unprepared statement with an equal, unprepared query
doesn't result in a syntax error:

***** CODE ****
Statement stmt = conn.createStatement();

ResultSet res = stmt.executeQuery(
        "SELECT EXTRACT (YEAR FROM DATE '1990-06-07 +02:00:00')");

res.next();

System.out.println("Year: " + res.getInt(1));
***** END CODE ****

A complete example that shows this bug is added at the end of the mail.

Regards,
Matthias Boehm


***** CODE *****

package bug;

import java.sql.*;
import java.util.*;

public class Bug {

    public static void main(String[] args) throws SQLException {

        // create connection...
        Connection conn = ...

        // ---- This does work ----
        Statement stmt = conn.createStatement();

        ResultSet res = stmt.executeQuery(
                "SELECT EXTRACT (YEAR FROM DATE '1990-06-07
+02:00:00')");

        res.next();

        System.out.println("Year: " + res.getInt(1));

        // ---- This doesn't work ----
        PreparedStatement stmt2 = conn.prepareStatement(
                "SELECT EXTRACT (YEAR FROM DATE ?)");

        java.sql.Date date = new java.sql.Date(
                new GregorianCalendar(1990, 5,
7).getTime().getTime());

        stmt2.setDate(1, date);

        // It doesn't work although the compiled statement with
values inserted
        // seems to be textually equal to the statement above:
        System.out.println(stmt2.toString());

        // here I get an SQL exception:
        res = stmt2.executeQuery();

        res.next();

        System.out.println("Year: " + res.getInt(1));


        conn.close();
    }

}


***** END CODE *****

Output of the program:

*****
Year: 1990
SELECT EXTRACT (YEAR FROM DATE '1990-06-07 +02:00:00')
Exception in thread "main" org.postgresql.util.PSQLException: FEHLER:
Syntaxfehler bei »$1«
  Position: 32
    at
org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorI
mpl.java:2102)
    at
org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.ja
va:1835)
    at
org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.j
ava:500)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2St
atement.java:388)
    at
org.postgresql.jdbc2.AbstractJdbc2Statement.executeQuery(AbstractJdbc2Statem
ent.java:273)
    at Bug.main(Bug.java:41)
*****


Re: Bug with PreparedStatements using EXTRACT function

From
Maciek Sakrejda
Date:
That's because the 'date' is not a cast but some sort of type
specification inherent to the expression. I don't understand the
details of the grammar, but you can see this if you try to run it via
PREPARE (the closest language-level thing you can use to JDBC's
parameterized statements):

cqdb=# prepare foo(date) as select extract (year from date '2000-01-01');
PREPARE
cqdb=# prepare bar(date) as select extract (year from date $1);
ERROR:  syntax error at or near "$1"
LINE 1: prepare bar(date) as select extract (year from date $1);
                                                            ^
cqdb=# prepare baz(date) as select extract (year from $1);
PREPARE
cqdb=#

---
Maciek Sakrejda | System Architect | Truviso

1065 E. Hillsdale Blvd., Suite 215
Foster City, CA 94404
(650) 242-3500 Main
www.truviso.com

Re: Bug with PreparedStatements using EXTRACT function

From
Matthias Böhm
Date:

> -----Ursprüngliche Nachricht-----
> Von: Maciek Sakrejda [mailto:msakrejda@truviso.com]
> Gesendet: Mittwoch, 28. März 2012 22:54
> An: Matthias Böhm
> Cc: pgsql-jdbc@postgresql.org
> Betreff: Re: [JDBC] Bug with PreparedStatements using EXTRACT function
>
> That's because the 'date' is not a cast but some sort of type
> specification inherent to the expression. I don't understand the
> details of the grammar, but you can see this if you try to run it via
> PREPARE (the closest language-level thing you can use to JDBC's
> parameterized statements):
>
> cqdb=# prepare foo(date) as select extract (year from date '2000-01-
> 01');
> PREPARE
> cqdb=# prepare bar(date) as select extract (year from date $1);
> ERROR:  syntax error at or near "$1"
> LINE 1: prepare bar(date) as select extract (year from date $1);
>                                                             ^
> cqdb=# prepare baz(date) as select extract (year from $1);
> PREPARE
> cqdb=#

OK, I found a solution:

Whereas the last prepare statement works on the sql console it doesn't work
with jdbc: I've tried to construct the PreparedStatement as follows:

PreparedStatement stmt2 = conn.prepareStatement(
        "SELECT EXTRACT (YEAR FROM ?)");

And now the exception I get is (translated roughly from German):

Function pg_catalog.date_part(unknown, unknown) is ambiguous.
Hint: Couldn't choose best candidate function. You may have to add an
explicit cast.

Adding this explicit cast makes the thing work:

PreparedStatement stmt2 = conn.prepareStatement(
         "SELECT EXTRACT (YEAR FROM CAST (? AS DATE))");


It is not obvious though at once that this cast is needed.

Thanks for taking a look at this issue and kind regards,
Matthias Böhm



Re: Bug with PreparedStatements using EXTRACT function

From
Maciek Sakrejda
Date:
>Whereas the last prepare statement works on the sql console...

Well, only if you explicitly provide the parameter type information,
as I did. Compare the last statement to

cqdb=# prepare quux as select extract(year from $1);
ERROR:  function pg_catalog.date_part(unknown, unknown) is not unique
LINE 1: prepare quux as select extract(year from $1);
                             ^
HINT:  Could not choose a best candidate function. You might need to
add explicit type casts.

Note that the parameter declaration on the prepared statement is
missing this time and you get a similar error.

>...it doesn't work with jdbc:

Are you using setObject() to provide the parameter value? If so, I
think the JDBC driver does not give the server enough information
about the target parameter type. It asks the server to figure it out,
and the server can't, because it's ambiguous if all it's getting is a
parameter string typed as "unknown". If you use one of the
type-specific setters, it should work. The driver does attempt to do
some type-guessing based on the Java "source" parameter type, but
there is something of a disconnect between Java and Postgres types, so
this does *not* occur for java.util.Date, and for java.sql.Timestamp,
it occurs in such a way as to break for your use case (see
https://github.com/pgjdbc/pgjdbc/blob/master/org/postgresql/jdbc2/AbstractJdbc2Statement.java#L3272
).

---
Maciek Sakrejda | System Architect | Truviso

1065 E. Hillsdale Blvd., Suite 215
Foster City, CA 94404
(650) 242-3500 Main
www.truviso.com

Re: Bug with PreparedStatements using EXTRACT function

From
Matthias Böhm
Date:
> If you use one of the type-specific setters, it should work.

No, it doesn't: I'm using setDate(int, java.util.sql.Date) or
setTimestamp(int, java.util.sql.Timestamp) for setting the parameter, but
the information about the parameter type seems to get lost:

A legal query without using a parameter is:

SELECT EXTRACT (YEAR FROM DATE '1990-06-07 +02:00:00')

But when I prepare a statement with "SELECT EXTRACT (YEAR FROM ?)" and use
the setDate method to set the parameter and after this, use the toString
method of the PreparedStatement, I get the following:

SELECT EXTRACT (YEAR FROM '1991-09-07 +02:00:00')

In this query the obviously required DATE is missing!

On the contrary, trying to prepare a statement with "SELECT EXTRACT (YEAR
FROM DATE ?)" results, after calling toString on the prepared statement, in
the following, seemingly correct query:

SELECT EXTRACT (YEAR FROM DATE '1991-09-07 +02:00:00')

But when I try to execute the query I get a "syntax error at $1".

So there seems no way to prepare such a query without using the explicit
cast.

> The driver does attempt to do
> some type-guessing based on the Java "source" parameter type, but
> there is something of a disconnect between Java and Postgres types, so
> this does *not* occur for java.util.Date, and for java.sql.Timestamp,
> it occurs in such a way as to break for your use case

Yes, I took a look at the code, and the problem seems to be that bindString
method is called with Oid.UNSPECIFIED, so that the information about the
type is lost.

Still it is not clear to me why preparing a statement with "SELECT EXTRACT
(YEAR FROM DATE ?)" doesn't work as well, because it yields, as already
mentioned above, a seemingly valid query.




Re: Bug with PreparedStatements using EXTRACT function

From
Maciek Sakrejda
Date:
I see--that attempt to be clever by the driver is actually the crux of
your problem. It's there to avoid issues like this (I'm in US/Pacific,
setting the time zone to US/Eastern for the test):

maciek@anemone:~/aux-git/pgjdbc$ cat no-tz.sql
set TimeZone to 'US/Eastern';
prepare s1(timestamp) as select extract(hour from $1);
execute s1('2012-01-01 12:00:00 +1:00');
maciek@anemone:~/aux-git/pgjdbc$ psql -f no-tz.sql
SET
PREPARE
 date_part
-----------
        12
(1 row)

maciek@anemone:~/aux-git/pgjdbc$ cat with-tz.sql
set TimeZone to 'US/Eastern';
prepare s1(timestamptz) as select extract(hour from $1);
execute s1('2012-01-01 12:00:00 +1:00');
maciek@anemone:~/aux-git/pgjdbc$ psql -f with-tz.sql
SET
PREPARE
 date_part
-----------
        6
(1 row)

>On the contrary, trying to prepare a statement with "SELECT EXTRACT (YEAR
>FROM DATE ?)" results, after calling toString on the prepared statement, in
>the following, seemingly correct query:
>
>SELECT EXTRACT (YEAR FROM DATE '1991-09-07 +02:00:00')
>
>But when I try to execute the query I get a "syntax error at $1".

Well, the toString() method is entirely client-side and it just
interpolates the stringified parameter values into the prepared
statement for display. Many drivers send that resulting string to the
server directly (the server doesn't see any parameters, just
literals), but the Postgres driver sends the original string with
parameter markers (well, munged to substitute JDBC-specific '?' with
Postgres-specific '$1', '$2', etc.) along with the actual parameter
values out of band. In theory, this is safer and faster (allowing
re-use of prepared plans). In practice it's great until it breaks down
in a case like yours ;)

That is, "date '2012-01-01'" is not a date literal by itself: again,
I'm not clear on the details of the grammar, but it's more like a
literal with a cast (e.g., you can do something like "select integer
'1'"). Because of that, you can't send that whole thing to the server
as a parameter value, because it's *only* expecting the value.

I'm not sure if there's a good solution to what you're seeing. If we
were starting from scratch, I'd push for ignoring timestamp entirely
in favor of the generally more sane timestamptz, but if we make that
change now, all sorts of subtle bugs will pop up (or rather, seep into
application interfaces unnoticed) for other users.

---
Maciek Sakrejda | System Architect | Truviso

1065 E. Hillsdale Blvd., Suite 215
Foster City, CA 94404
(650) 242-3500 Main
www.truviso.com