Sources of this patch: https://github.com/pgjdbc/pgjdbc/commit/b9b3777671c8a5cc580e1985f61337d39d47c730 https://github.com/pgjdbc/pgjdbc/commit/990d63f6be401ab40de5eb303a75924c9e71903c diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java index 1ce49996..b1bbb41a 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleParameterList.java @@ -168,99 +170,163 @@ class SimpleParameterList implements V3ParameterList { bind(index, NULL_OBJECT, oid, binaryTransfer); } + /** + *

Escapes a given text value as a literal, wraps it in single quotes, casts it to the + * to the given data type, and finally wraps the whole thing in parentheses.

+ * + *

For example, "123" and "int4" becomes "('123'::int)"

+ * + *

The additional parentheses is added to ensure that the surrounding text of where the + * parameter value is entered does modify the interpretation of the value.

+ * + *

For example if our input SQL is: SELECT ?b

+ * + *

Using a parameter value of '{}' and type of json we'd get:

+ * + *
+   * test=# SELECT ('{}'::json)b;
+   *  b
+   * ----
+   *  {}
+   * 
+ * + *

But without the parentheses the result changes:

+ * + *
+   * test=# SELECT '{}'::jsonb;
+   * jsonb
+   * -------
+   * {}
+   * 
+ **/ + private static String quoteAndCast(String text, String type, boolean standardConformingStrings) { + StringBuilder sb = new StringBuilder((text.length() + 10) / 10 * 11); // Add 10% for escaping. + sb.append("('"); + try { + Utils.escapeLiteral(sb, text, standardConformingStrings); + } catch (SQLException e) { + // This should only happen if we have an embedded null + // and there's not much we can do if we do hit one. + // + // To force a server side failure, we deliberately include + // a zero byte character in the literal to force the server + // to reject the command. + sb.append('\u0000'); + } + sb.append("'"); + if (type != null) { + sb.append("::"); + sb.append(type); + } + sb.append(")"); + return sb.toString(); + } + @Override public String toString(int index, boolean standardConformingStrings) { --index; if (paramValues[index] == null) { return "?"; } else if (paramValues[index] == NULL_OBJECT) { - return "NULL"; - } else if ((flags[index] & BINARY) == BINARY) { + return "(NULL)"; + } + String textValue; + String type; + if ((flags[index] & BINARY) == BINARY) { // handle some of the numeric types - switch (paramTypes[index]) { case Oid.INT2: short s = ByteConverter.int2((byte[]) paramValues[index], 0); - return Short.toString(s); + textValue = Short.toString(s); + type = "int2"; + break; case Oid.INT4: int i = ByteConverter.int4((byte[]) paramValues[index], 0); - return Integer.toString(i); + textValue = Integer.toString(i); + type = "int4"; + break; case Oid.INT8: long l = ByteConverter.int8((byte[]) paramValues[index], 0); - return Long.toString(l); + textValue = Long.toString(l); + type = "int8"; + break; case Oid.FLOAT4: float f = ByteConverter.float4((byte[]) paramValues[index], 0); if (Float.isNaN(f)) { - return "'NaN'::real"; + return "('NaN'::real)"; } - return Float.toString(f); + textValue = Float.toString(f); + type = "real"; + break; case Oid.FLOAT8: double d = ByteConverter.float8((byte[]) paramValues[index], 0); if (Double.isNaN(d)) { - return "'NaN'::double precision"; + return "('NaN'::double precision)"; + } + textValue = Double.toString(d); + type = "double precision"; + break; + + case Oid.NUMERIC: + Number n = ByteConverter.numeric((byte[]) paramValues[index]); + if (n instanceof Double) { + assert ((Double) n).isNaN(); + return "('NaN'::numeric)"; } - return Double.toString(d); + textValue = n.toString(); + type = "numeric"; + break; case Oid.UUID: - String uuid = + textValue = new UUIDArrayAssistant().buildElement((byte[]) paramValues[index], 0, 16).toString(); - return "'" + uuid + "'::uuid"; + type = "uuid"; + break; case Oid.POINT: PGpoint pgPoint = new PGpoint(); pgPoint.setByteValue((byte[]) paramValues[index], 0); - return "'" + pgPoint.toString() + "'::point"; + textValue = pgPoint.toString(); + type = "point"; + break; case Oid.BOX: PGbox pgBox = new PGbox(); pgBox.setByteValue((byte[]) paramValues[index], 0); - return "'" + pgBox.toString() + "'::box"; + textValue = pgBox.toString(); + type = "box"; + break; + + default: + return "?"; } - return "?"; } else { - String param = paramValues[index].toString(); - - // add room for quotes + potential escaping. - StringBuilder p = new StringBuilder(3 + (param.length() + 10) / 10 * 11); - - // No E'..' here since escapeLiteral escapes all things and it does not use \123 kind of - // escape codes - p.append('\''); - try { - p = Utils.escapeLiteral(p, param, standardConformingStrings); - } catch (SQLException sqle) { - // This should only happen if we have an embedded null - // and there's not much we can do if we do hit one. - // - // The goal of toString isn't to be sent to the server, - // so we aren't 100% accurate (see StreamWrapper), put - // the unescaped version of the data. - // - p.append(param); - } - p.append('\''); + textValue = paramValues[index].toString(); + int paramType = paramTypes[index]; if (paramType == Oid.TIMESTAMP) { - p.append("::timestamp"); + type = "timestamp"; } else if (paramType == Oid.TIMESTAMPTZ) { - p.append("::timestamp with time zone"); + type = "timestamp with time zone"; } else if (paramType == Oid.TIME) { - p.append("::time"); + type = "time"; } else if (paramType == Oid.TIMETZ) { - p.append("::time with time zone"); + type = "time with time zone"; } else if (paramType == Oid.DATE) { - p.append("::date"); + type = "date"; } else if (paramType == Oid.INTERVAL) { - p.append("::interval"); + type = "interval"; } else if (paramType == Oid.NUMERIC) { - p.append("::numeric"); + type = "numeric"; + } else { + type = null; } - return p.toString(); } + return quoteAndCast(textValue, type, standardConformingStrings); } @Override