From cdec924d1e3e87936f7225931eea163010543b28 Mon Sep 17 00:00:00 2001 From: lukaseder Date: Thu, 5 Oct 2017 15:09:34 +0200 Subject: [PATCH] [#4930] [#6631] DefaultBinding refactoring - [#4930] Create one DefaultXYZBinding type per DataType, internally - [#6631] Deprecate public DefaultBinding constructor --- .../java/org/jooq/impl/ArrayRecordImpl.java | 1 + .../org/jooq/impl/DateAsTimestampBinding.java | 6 +- .../java/org/jooq/impl/DefaultBinding.java | 3874 +++++++++++------ .../java/org/jooq/impl/DefaultDataType.java | 8 +- .../src/main/java/org/jooq/impl/RowField.java | 6 +- jOOQ/src/main/java/org/jooq/impl/Tools.java | 34 + 6 files changed, 2480 insertions(+), 1449 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/ArrayRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/ArrayRecordImpl.java index 45e6f057be..7df5e009a9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ArrayRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ArrayRecordImpl.java @@ -399,6 +399,7 @@ + \ No newline at end of file diff --git a/jOOQ/src/main/java/org/jooq/impl/DateAsTimestampBinding.java b/jOOQ/src/main/java/org/jooq/impl/DateAsTimestampBinding.java index 76a40f9c36..bceff7e434 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DateAsTimestampBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DateAsTimestampBinding.java @@ -69,14 +69,14 @@ public class DateAsTimestampBinding implements Binding { /** * Generated UID */ - private static final long serialVersionUID = -7730882831126647188L; + private static final long serialVersionUID = -7730882831126647188L; private final Converter converter; - private final DefaultBinding delegate; + private final Binding delegate; public DateAsTimestampBinding() { this.converter = Converters.identity(Timestamp.class); - this.delegate = new DefaultBinding(converter); + this.delegate = DefaultBinding.binding(converter); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java index eeb6ee2fbb..79ad47ed4d 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java @@ -55,7 +55,6 @@ import static org.jooq.SQLDialect.SQLITE; // ... // ... -// ... import static org.jooq.conf.ParamType.INLINED; import static org.jooq.impl.DSL.cast; import static org.jooq.impl.DSL.inline; @@ -80,6 +79,7 @@ import static org.jooq.impl.Keywords.K_YEAR_TO_DAY; import static org.jooq.impl.Keywords.K_YEAR_TO_FRACTION; import static org.jooq.impl.Tools.attachRecords; +import static org.jooq.impl.Tools.convertBytesToHex; import static org.jooq.impl.Tools.getMappedUDTName; import static org.jooq.impl.Tools.needsBackslashEscaping; import static org.jooq.tools.jdbc.JDBCUtils.safeClose; @@ -156,7 +156,6 @@ import org.jooq.types.UByte; import org.jooq.types.UInteger; import org.jooq.types.ULong; -import org.jooq.types.UNumber; import org.jooq.types.UShort; import org.jooq.types.YearToMonth; import org.jooq.util.postgres.PostgresUtils; @@ -167,29 +166,133 @@ public class DefaultBinding implements Binding { static final JooqLogger log = JooqLogger.getLogger(DefaultBinding.class); - private static final char[] HEX = "0123456789abcdef".toCharArray(); /** * Generated UID */ private static final long serialVersionUID = -198499389344950496L; - final Class type; - final Converter converter; + final AbstractBinding delegate; - @Deprecated - // TODO: This type boolean should not be passed standalone to the - // constructor. Find a better design - final boolean isLob; + public final static Binding binding(Converter converter) { + return binding(converter, false); + } + + static final Binding binding(Class type, boolean isLob) { + return binding(Converters.identity(type), isLob); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + static final Binding binding(Converter converter, boolean isLob) { + Class type = converter.fromType(); + + // Concrete types + if (type == BigDecimal.class) + return new DefaultBigDecimalBinding(converter, isLob); + else if (type == BigInteger.class) + return new DefaultBigIntegerBinding(converter, isLob); + else if (type == Blob.class) + return new DefaultBlobBinding(converter, isLob); + else if (type == Boolean.class) + return new DefaultBooleanBinding(converter, isLob); + else if (type == Byte.class || type == byte.class) + return new DefaultByteBinding(converter, isLob); + else if (type == byte[].class) + return new DefaultBytesBinding(converter, isLob); + else if (type == Clob.class) + return new DefaultClobBinding(converter, isLob); + else if (type == Date.class) + return new DefaultDateBinding(converter, isLob); + else if (type == DayToSecond.class) + return new DefaultDayToSecondBinding(converter, isLob); + else if (type == Double.class || type == double.class) + return new DefaultDoubleBinding(converter, isLob); + else if (type == Float.class || type == float.class) + return new DefaultFloatBinding(converter, isLob); + else if (type == Integer.class || type == int.class) + return new DefaultIntegerBinding(converter, isLob); + + else if (type == LocalDate.class) { + DateToLocalDateConverter c1 = new DateToLocalDateConverter(); + Converter c2 = (Converter) converter; + Converter c3 = Converters.of(c1, c2); + return (Binding) new DelegatingBinding(c1, c2, new DefaultDateBinding(c3, isLob), isLob); + } + else if (type == LocalDateTime.class) { + TimestampToLocalDateTimeConverter c1 = new TimestampToLocalDateTimeConverter(); + Converter c2 = (Converter) converter; + Converter c3 = Converters.of(c1, c2); + return (Binding) new DelegatingBinding(c1, c2, new DefaultTimestampBinding(c3, isLob), isLob); + } + else if (type == LocalTime.class) { + TimeToLocalTimeConverter c1 = new TimeToLocalTimeConverter(); + Converter c2 = (Converter) converter; + Converter c3 = Converters.of(c1, c2); + return (Binding) new DelegatingBinding(c1, c2, new DefaultTimeBinding(c3, isLob), isLob); + } + + else if (type == Long.class || type == long.class) + return new DefaultLongBinding(converter, isLob); + + else if (type == OffsetDateTime.class) + return new DefaultOffsetDateTimeBinding(converter, isLob); + else if (type == OffsetTime.class) + return new DefaultOffsetTimeBinding(converter, isLob); + + else if (type == Short.class || type == short.class) + return new DefaultShortBinding(converter, isLob); + else if (type == String.class) + return new DefaultStringBinding(converter, isLob); + else if (type == Time.class) + return new DefaultTimeBinding(converter, isLob); + else if (type == Timestamp.class) + return new DefaultTimestampBinding(converter, isLob); + else if (type == UByte.class) + return new DefaultUByteBinding(converter, isLob); + else if (type == UInteger.class) + return new DefaultUIntegerBinding(converter, isLob); + else if (type == ULong.class) + return new DefaultULongBinding(converter, isLob); + else if (type == UShort.class) + return new DefaultUShortBinding(converter, isLob); + else if (type == UUID.class) + return new DefaultUUIDBinding(converter, isLob); + else if (type == YearToMonth.class) + return new DefaultYearToMonthBinding(converter, isLob); + + // Subtypes of array types etc. + // The type byte[] is handled earlier. byte[][] can be handled here + else if (type.isArray()) + return new DefaultArrayBinding(converter, isLob); + else if (ArrayRecord.class.isAssignableFrom(type)) + return new DefaultArrayRecordBinding(converter, isLob); + else if (EnumType.class.isAssignableFrom(type)) + return new DefaultEnumTypeBinding(converter, isLob); + else if (Record.class.isAssignableFrom(type)) + return new DefaultRecordBinding(converter, isLob); + else if (Result.class.isAssignableFrom(type)) + return new DefaultResultBinding(converter, isLob); + + // Undefined types + else + return new DefaultOtherBinding(converter, isLob); + + } + /** + * @deprecated - 3.11 - [#6631] - Use {@link #binding(Converter)} instead. + */ + @Deprecated public DefaultBinding(Converter converter) { this(converter, false); } + /** + * @deprecated - 3.11 - [#6631] - Use {@link #binding(Converter)} instead. + */ + @Deprecated DefaultBinding(Converter converter, boolean isLob) { - this.type = converter.fromType(); - this.converter = converter; - this.isLob = isLob; + this.delegate = (AbstractBinding) binding(converter, isLob); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -204,7 +307,7 @@ else if (converter == null) { theBinding = (Binding) binding; } else if (binding == null) { - theBinding = (Binding) new DefaultBinding(converter, type.isLob()); + theBinding = (Binding) binding(converter, type.isLob()); } else { theBinding = new Binding() { @@ -261,306 +364,255 @@ public void get(BindingGetSQLInputContext ctx) throws SQLException { return theBinding; } - @Override - public Converter converter() { - return converter; + static final Map> typeMap(Class type, Configuration configuration) { + return typeMap(type, configuration, new HashMap>()); } - @Override - public void sql(BindingSQLContext ctx) { - T converted = converter.to(ctx.value()); - - // Casting can be enforced or prevented - switch (ctx.render().castMode()) { - case NEVER: - toSQL(ctx, converted); - return; + @SuppressWarnings("unchecked") + static final Map> typeMap(Class type, Configuration configuration, Map> result) { + try { + if (UDTRecord.class.isAssignableFrom(type)) { + Class> t = (Class>) type; + result.put(getMappedUDTName(configuration, t), t); + UDTRecord r = t.newInstance(); + for (Field field : r.getUDT().fields()) + typeMap(field.getType(), configuration, result); + } - case ALWAYS: - toSQLCast(ctx, converted); - return; - } - // See if we "should" cast, to stay on the safe side - if (shouldCast(ctx, converted)) { - toSQLCast(ctx, converted); - } - // Most RDBMS can infer types for bind values - else { - toSQL(ctx, converted); - } - } - private final boolean shouldCast(BindingSQLContext ctx, T converted) { - // In default mode, casting is only done when parameters are NOT inlined - if (ctx.render().paramType() != INLINED) { - // Generated enums should not be cast... - if (!(converted instanceof EnumType)) { - switch (ctx.family()) { - // These dialects can hardly detect the type of a bound constant. + } + catch (Exception e) { + throw new MappingException("Error while collecting type map", e); + } + return result; + } - case DERBY: - case FIREBIRD: + private static final long parse(Class type, String date) throws SQLException { - // These dialects have some trouble, when they mostly get it right. - case H2: - case HSQLDB: + // Try reading a plain number first + try { + return Long.valueOf(date); + } - // [#1261] There are only a few corner-cases, where this is - // really needed. Check back on related CUBRID bugs - case CUBRID: + // If that fails, try reading a formatted date + catch (NumberFormatException e) { - // [#1029] Postgres and [#632] Sybase need explicit casting - // in very rare cases. + if (type == Timestamp.class) + return Timestamp.valueOf(date).getTime(); + // Dates may come with " 00:00:00". This is safely trimming time information + if (type == Date.class) + return Date.valueOf(date.split(" ")[0]).getTime(); + if (type == Time.class) + return Time.valueOf(date).getTime(); + throw new SQLException("Could not parse date " + date, e); + } + } + // ----------------------------------------------------------------------------------------------------------------- + // Implementation of Binding API for backwards compatibility + // ----------------------------------------------------------------------------------------------------------------- - case POSTGRES: { - return true; - } - } - } - } + @Override + public Converter converter() { + return delegate.converter; + } - // [#566] JDBC doesn't explicitly support interval data types. To be on - // the safe side, always cast these types in those dialects that support - // them - if (Interval.class.isAssignableFrom(type)) { - switch (ctx.family()) { + @Override + public void sql(BindingSQLContext ctx) throws SQLException { + delegate.sql(ctx); + } + @Override + public void register(BindingRegisterContext ctx) throws SQLException { + delegate.register(ctx); + } + @Override + public void set(BindingSetStatementContext ctx) throws SQLException { + delegate.set(ctx); + } - case POSTGRES: - return true; - } - } + @Override + public void set(BindingSetSQLOutputContext ctx) throws SQLException { + delegate.set(ctx); + } - return false; + @Override + public void get(BindingGetResultSetContext ctx) throws SQLException { + delegate.get(ctx); + } + @Override + public void get(BindingGetStatementContext ctx) throws SQLException { + delegate.get(ctx); } - /** - * Render the bind variable including a cast, if necessary - */ - private final void toSQLCast(BindingSQLContext ctx, T converted) { - DataType dataType = DefaultDataType.getDataType(ctx.dialect(), type); - DataType sqlDataType = dataType.getSQLDataType(); - SQLDialect family = ctx.family(); + @Override + public void get(BindingGetSQLInputContext ctx) throws SQLException { + delegate.get(ctx); + } - // [#822] Some RDBMS need precision / scale information on BigDecimals - if (converted != null && type == BigDecimal.class && asList(CUBRID, DERBY, FIREBIRD, HSQLDB).contains(family)) { + // ----------------------------------------------------------------------------------------------------------------- + // Object API + // ----------------------------------------------------------------------------------------------------------------- - // Add precision / scale on BigDecimals - int scale = ((BigDecimal) converted).scale(); - int precision = ((BigDecimal) converted).precision(); + @Override + public String toString() { + return "DefaultBinding [type=" + delegate.type + ", converter=" + delegate.converter + "]"; + } - // [#5323] BigDecimal precision is always 1 for BigDecimals smaller than 1.0 - if (scale >= precision) - precision = scale + 1; + // ----------------------------------------------------------------------------------------------------------------- + // Type-specific subclasses API + // ----------------------------------------------------------------------------------------------------------------- - toSQLCast(ctx, converted, dataType, 0, precision, scale); - } + abstract static class AbstractBinding implements Binding { - // [#1028] Most databases don't know an OTHER type (except H2, HSQLDB). - else if (SQLDataType.OTHER == sqlDataType) { + /** + * Generated UID + */ + private static final long serialVersionUID = -7965247586545864991L; - // If the bind value is set, it can be used to derive the cast type - if (converted != null) { - toSQLCast(ctx, converted, DefaultDataType.getDataType(family, converted.getClass()), 0, 0, 0); - } + final Class type; + final Converter converter; - // [#632] [#722] Current integration tests show that Ingres and - // Sybase can do without casting in most cases. - else if (asList().contains(family)) { - ctx.render().sql(ctx.variable()); - } + // TODO: This type boolean should not be passed standalone to the + // constructor. Find a better design + final boolean isLob; - // Derby and DB2 must have a type associated with NULL. Use VARCHAR - // as a workaround. That's probably not correct in all cases, though - else { - toSQLCast(ctx, converted, DefaultDataType.getDataType(family, String.class), 0, 0, 0); - } + AbstractBinding(Converter converter, boolean isLob) { + this.type = converter.fromType(); + this.converter = converter; + this.isLob = isLob; } - // [#1029] Postgres generally doesn't need the casting. Only in the - // above case where the type is OTHER - // [#1125] Also with temporal data types, casting is needed some times - // [#4338] ... specifically when using JSR-310 types - // [#1130] TODO type can be null for ARRAY types, etc. - else if (asList(POSTGRES).contains(family) && (sqlDataType == null || !sqlDataType.isTemporal())) { - toSQL(ctx, converted); + @Override + public final Converter converter() { + return converter; } - // [#1727] VARCHAR types should be cast to their actual lengths in some - // dialects - else if ((sqlDataType == SQLDataType.VARCHAR || sqlDataType == SQLDataType.CHAR) && asList(FIREBIRD).contains(family)) { - toSQLCast(ctx, converted, dataType, getValueLength((String) converted), 0, 0); - } + private final boolean shouldCast(BindingSQLContext ctx, T converted) { + // In default mode, casting is only done when parameters are NOT inlined + if (ctx.render().paramType() != INLINED) { + // Generated enums should not be cast... + if (!(converted instanceof EnumType)) { + switch (ctx.family()) { + // These dialects can hardly detect the type of a bound constant. + case DERBY: + case FIREBIRD: - // In all other cases, the bind variable can be cast normally - else { - toSQLCast(ctx, converted, dataType, dataType.length(), dataType.precision(), dataType.scale()); - } - } + // These dialects have some trouble, when they mostly get it right. + case H2: + case HSQLDB: - private static final int getValueLength(String string) { - if (string == null) { - return 1; - } + // [#1261] There are only a few corner-cases, where this is + // really needed. Check back on related CUBRID bugs + case CUBRID: - else { - int length = string.length(); - - // If non 7-bit ASCII characters are present, multiply the length by - // 4 to be sure that even UTF-32 collations will fit. But don't use - // larger numbers than Derby's upper limit 32672 - for (int i = 0; i < length; i++) { - if (string.charAt(i) > 127) { - return Math.min(32672, 4 * length); - } - } + // [#1029] Postgres and [#632] Sybase need explicit casting + // in very rare cases. - return Math.min(32672, length); - } - } - private final void toSQLCast(BindingSQLContext ctx, T converted, DataType dataType, int length, int precision, int scale) { - ctx.render().visit(K_CAST).sql('('); - toSQL(ctx, converted); - ctx.render().sql(' ').visit(K_AS).sql(' ') - .sql(dataType.length(length).precision(precision, scale).getCastTypeName(ctx.configuration())) - .sql(')'); - } - /** - * Inlining abstraction - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - private final void toSQL(BindingSQLContext ctx, Object val) { - SQLDialect family = ctx.family(); - RenderContext render = ctx.render(); - - if (render.paramType() == INLINED) { - // [#2223] Some type-casts in this section may seem unnecessary, e.g. - // ((Boolean) val).toString(). They have been put in place to avoid - // accidental type confusions where type != val.getClass(), and thus - // SQL injection may occur - - if (val == null) { - render.visit(K_NULL); - } - else if (type == Boolean.class) { - // [#1153] Some dialects don't support boolean literals TRUE and FALSE - if (asList(FIREBIRD, SQLITE).contains(family)) { - render.sql(((Boolean) val) ? "1" : "0"); - } + case POSTGRES: { + return true; + } + } + } + } + // [#566] JDBC doesn't explicitly support interval data types. To be on + // the safe side, always cast these types in those dialects that support + // them + if (Interval.class.isAssignableFrom(type)) { + switch (ctx.family()) { - else { - render.visit(((Boolean) val) ? K_TRUE : K_FALSE); + case POSTGRES: + return true; } } - // [#1154] Binary data cannot always be inlined - else if (type == byte[].class) { - byte[] binary = (byte[]) val; - - if (asList().contains(family)) { - render.sql("0x") - .sql(convertBytesToHex(binary)); - } - + return false; + } + /** + * Render the bind variable including a cast, if necessary + */ + private final void sqlCast(BindingSQLContext ctx, T converted) throws SQLException { + DataType dataType = DefaultDataType.getDataType(ctx.dialect(), type); + DataType sqlDataType = dataType.getSQLDataType(); + SQLDialect family = ctx.family(); + // [#822] Some RDBMS need precision / scale information on BigDecimals + if (converted != null && type == BigDecimal.class && asList(CUBRID, DERBY, FIREBIRD, HSQLDB).contains(family)) { + // Add precision / scale on BigDecimals + int scale = ((BigDecimal) converted).scale(); + int precision = ((BigDecimal) converted).precision(); + // [#5323] BigDecimal precision is always 1 for BigDecimals smaller than 1.0 + if (scale >= precision) + precision = scale + 1; + sqlCast(ctx, converted, dataType, 0, precision, scale); + } + // [#1028] Most databases don't know an OTHER type (except H2, HSQLDB). + else if (SQLDataType.OTHER == sqlDataType) { - else if (asList(DERBY, H2, HSQLDB, MARIADB, MYSQL, SQLITE).contains(family)) { - render.sql("X'") - .sql(convertBytesToHex(binary)) - .sql('\''); + // If the bind value is set, it can be used to derive the cast type + if (converted != null) { + sqlCast(ctx, converted, DefaultDataType.getDataType(family, converted.getClass()), 0, 0, 0); } + + // [#632] [#722] Current integration tests show that Ingres and + // Sybase can do without casting in most cases. else if (asList().contains(family)) { - render.sql("hextoraw('") - .sql(convertBytesToHex(binary)) - .sql("')"); - } - else if (family == POSTGRES) { - render.sql("E'") - .sql(PostgresUtils.toPGString(binary)) - .sql("'::bytea"); + ctx.render().sql(ctx.variable()); } - // This default behaviour is used in debug logging for dialects - // that do not support inlining binary data + // Derby and DB2 must have a type associated with NULL. Use VARCHAR + // as a workaround. That's probably not correct in all cases, though else { - render.sql("X'") - .sql(convertBytesToHex(binary)) - .sql('\''); + sqlCast(ctx, converted, DefaultDataType.getDataType(family, String.class), 0, 0, 0); } } - // Interval extends Number, so let Interval come first! - else if (Interval.class.isAssignableFrom(type)) { - render.sql('\'') - .sql(escape(val, render)) - .sql('\''); - } - - // [#5249] Special inlining of special floating point values - else if (Double.class.isAssignableFrom(type) && ((Double) val).isNaN()) { - if (POSTGRES == family) - render.visit(inline("NaN")).sql("::float8"); - else - render.sql(((Number) val).toString()); - } - - // [#5249] Special inlining of special floating point values - else if (Float.class.isAssignableFrom(type) && ((Float) val).isNaN()) { - if (POSTGRES == family) - render.visit(inline("NaN")).sql("::float4"); - else - render.sql(((Number) val).toString()); + // [#1029] Postgres generally doesn't need the casting. Only in the + // above case where the type is OTHER + // [#1125] Also with temporal data types, casting is needed some times + // [#4338] ... specifically when using JSR-310 types + // [#1130] TODO type can be null for ARRAY types, etc. + else if (asList(POSTGRES).contains(family) && (sqlDataType == null || !sqlDataType.isTemporal())) { + sql(ctx, converted); } - else if (Number.class.isAssignableFrom(type)) { - render.sql(((Number) val).toString()); + // [#1727] VARCHAR types should be cast to their actual lengths in some + // dialects + else if ((sqlDataType == SQLDataType.VARCHAR || sqlDataType == SQLDataType.CHAR) && asList(FIREBIRD).contains(family)) { + sqlCast(ctx, converted, dataType, getValueLength((String) converted), 0, 0); } - // [#1156] DATE / TIME inlining is very vendor-specific - else if (Tools.isDate(type)) { - Date date = getDate(type, val); - - // The SQLite JDBC driver does not implement the escape syntax - // [#1253] Sybase does not implement date literals - if (asList(SQLITE).contains(family)) { - render.sql('\'').sql(escape(date, render)).sql('\''); - } - - - - @@ -568,254 +620,486 @@ else if (Tools.isDate(type)) { + // In all other cases, the bind variable can be cast normally + else { + sqlCast(ctx, converted, dataType, dataType.length(), dataType.precision(), dataType.scale()); + } + } + private static final int getValueLength(String string) { + if (string == null) { + return 1; + } + else { + int length = string.length(); + + // If non 7-bit ASCII characters are present, multiply the length by + // 4 to be sure that even UTF-32 collations will fit. But don't use + // larger numbers than Derby's upper limit 32672 + for (int i = 0; i < length; i++) { + if (string.charAt(i) > 127) { + return Math.min(32672, 4 * length); + } + } + return Math.min(32672, length); + } + } + private final void sqlCast(BindingSQLContext ctx, T converted, DataType dataType, int length, int precision, int scale) throws SQLException { + ctx.render().visit(K_CAST).sql('('); + sql(ctx, converted); + ctx.render().sql(' ').visit(K_AS).sql(' ') + .sql(dataType.length(length).precision(precision, scale).getCastTypeName(ctx.configuration())) + .sql(')'); + } + @Override + public final void sql(BindingSQLContext ctx) throws SQLException { + T converted = converter().to(ctx.value()); - // [#1253] Derby doesn't support the standard literal - else if (family == DERBY) { - render.visit(K_DATE).sql("('").sql(escape(date, render)).sql("')"); - } + // Casting can be enforced or prevented + switch (ctx.render().castMode()) { + case NEVER: + sql(ctx, converted); + return; - // [#3648] Circumvent a MySQL bug related to date literals - else if (family == MYSQL) { - render.sql("{d '").sql(escape(date, render)).sql("'}"); - } + case ALWAYS: + sqlCast(ctx, converted); + return; + } - // Most dialects implement SQL standard date literals - else { - render.visit(K_DATE).sql(" '").sql(escape(date, render)).sql('\''); - } + // See if we "should" cast, to stay on the safe side + if (shouldCast(ctx, converted)) { + sqlCast(ctx, converted); } - else if (Tools.isTimestamp(type)) { - Timestamp ts = getTimestamp(type, val); - // The SQLite JDBC driver does not implement the escape syntax - // [#1253] Sybase does not implement timestamp literals - if (asList(SQLITE).contains(family)) { - render.sql('\'').sql(escape(ts, render)).sql('\''); - } + // Most RDBMS can infer types for bind values + else { + sql(ctx, converted); + } + } + private final void sql(BindingSQLContext ctx, T value) throws SQLException { + if (ctx.render().paramType() == INLINED) + if (value == null) + ctx.render().visit(K_NULL); + else + sqlInline0(ctx, value); + else + sqlBind0(ctx, value); + } + /** + * Escape a string literal by replacing ' by '', and possibly also backslashes. + */ + static final String escape(Object val, Context context) { + String result = val.toString(); + if (needsBackslashEscaping(context.configuration())) + result = result.replace("\\", "\\\\"); + return result.replace("'", "''"); + } + @Override + public final void register(BindingRegisterContext ctx) throws SQLException { + if (log.isTraceEnabled()) + log.trace("Registering variable " + ctx.index(), "" + type); + register0(ctx); + } + @Override + public final void set(BindingSetStatementContext ctx) throws SQLException { + T value = converter().to(ctx.value()); + if (log.isTraceEnabled()) + if (value != null && value.getClass().isArray() && value.getClass() != byte[].class) + log.trace("Binding variable " + ctx.index(), Arrays.asList((Object[]) value) + " (" + type + ")"); + else + log.trace("Binding variable " + ctx.index(), value + " (" + type + ")"); + if (value == null) + setNull0(ctx); + else + set0(ctx, value); + } + @Override + public final void set(BindingSetSQLOutputContext ctx) throws SQLException { + T value = converter().to(ctx.value()); + if (value == null) + ctx.output().writeObject(null); + else + set0(ctx, value); + } + @Override + public final void get(BindingGetResultSetContext ctx) throws SQLException { + ctx.value(attach(converter().from(get0(ctx)), ctx.configuration())); + } + @Override + public final void get(BindingGetStatementContext ctx) throws SQLException { + ctx.value(attach(converter().from(get0(ctx)), ctx.configuration())); + } + @Override + public final void get(BindingGetSQLInputContext ctx) throws SQLException { + ctx.value(attach(converter().from(get0(ctx)), ctx.configuration())); + } + private static final U attach(U value, Configuration configuration) { + // [#4372] Attach records if possible / required + if (value instanceof Attachable && attachRecords(configuration)) + ((Attachable) value).attach(configuration); - // [#1253] Derby doesn't support the standard literal - else if (family == DERBY) { - render.visit(K_TIMESTAMP).sql("('").sql(escape(ts, render)).sql("')"); - } + return value; + } - // CUBRID timestamps have no fractional seconds - else if (family == CUBRID) { - render.visit(K_DATETIME).sql(" '").sql(escape(ts, render)).sql('\''); - } + final Class type() { + return type; + } - // [#3648] Circumvent a MySQL bug related to date literals - else if (family == MYSQL) { - render.sql("{ts '").sql(escape(ts, render)).sql("'}"); - } + /* non-final */ void setNull0(BindingSetStatementContext ctx) throws SQLException { + ctx.statement().setNull(ctx.index(), sqltype(ctx.configuration())); + } - // Most dialects implement SQL standard timestamp literals - else { - render.visit(K_TIMESTAMP).sql(" '").sql(escape(ts, render)).sql('\''); - } - } - else if (Tools.isTime(type)) { - Time time = getTime(type, val); + /* non-final */ void register0(BindingRegisterContext ctx) throws SQLException { + ctx.statement().registerOutParameter(ctx.index(), sqltype(ctx.configuration())); + } - // The SQLite JDBC driver does not implement the escape syntax - // [#1253] Sybase does not implement time literals - if (asList(SQLITE).contains(family)) { - render.sql('\'').sql(new SimpleDateFormat("HH:mm:ss").format(time)).sql('\''); - } + @SuppressWarnings("unused") + /* non-final */ void sqlInline0(BindingSQLContext ctx, T value) throws SQLException { + // Known fall-through types: + // - Blob, Clob + // - String + // - UUID + ctx.render().sql('\'') + .sql(escape(value, ctx.render()), true) + .sql('\''); + } + @SuppressWarnings("unused") + /* non-final */ void sqlBind0(BindingSQLContext ctx, T value) throws SQLException { + ctx.render().sql(ctx.variable()); + } + // abstract void register0(BindingRegisterContext ctx) throws SQLException; + abstract void set0(BindingSetStatementContext ctx, T value) throws SQLException; + abstract void set0(BindingSetSQLOutputContext ctx, T value) throws SQLException; + abstract T get0(BindingGetResultSetContext ctx) throws SQLException; + abstract T get0(BindingGetStatementContext ctx) throws SQLException; + abstract T get0(BindingGetSQLInputContext ctx) throws SQLException; + abstract int sqltype(Configuration configuration) throws SQLException; + // ----------------------------------------------------------------------------------------------------------------- + // Object API + // ----------------------------------------------------------------------------------------------------------------- + @Override + public String toString() { + return "AbstractBinding [type=" + type + ", converter=" + converter + "]"; + } + } + static final class DelegatingBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 9222783475194108822L; + private final Converter delegatingConverter; + private final AbstractBinding delegatingBinding; + DelegatingBinding( + Converter delegatingConverter, + Converter originalConverter, + AbstractBinding delegatingBinding, + boolean isLob + ) { + super(originalConverter, isLob); + this.delegatingConverter = delegatingConverter; + this.delegatingBinding = delegatingBinding; + } - // [#1253] Derby doesn't support the standard literal - else if (family == DERBY) { - render.visit(K_TIME).sql("('").sql(escape(time, render)).sql("')"); - } + @Override + final void sqlInline0(BindingSQLContext ctx, X value) throws SQLException { + delegatingBinding.sqlInline0(ctx, delegatingConverter.to(value)); + } - // [#3648] Circumvent a MySQL bug related to date literals - else if (family == MYSQL) { - render.sql("{t '").sql(escape(time, render)).sql("'}"); - } + @Override + final void sqlBind0(BindingSQLContext ctx, X value) throws SQLException { + delegatingBinding.sqlBind0(ctx, delegatingConverter.to(value)); + } + @Override + final void set0(BindingSetStatementContext ctx, X value) throws SQLException { + delegatingBinding.set0(ctx, delegatingConverter.to(value)); + } + @Override + final void setNull0(BindingSetStatementContext ctx) throws SQLException { + delegatingBinding.setNull0(ctx); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, X value) throws SQLException { + delegatingBinding.set0(ctx, delegatingConverter.to(value)); + } + @Override + final X get0(BindingGetResultSetContext ctx) throws SQLException { + return delegatingConverter.from(delegatingBinding.get0(ctx)); + } + @Override + final X get0(BindingGetStatementContext ctx) throws SQLException { + return delegatingConverter.from(delegatingBinding.get0(ctx)); + } + @Override + final X get0(BindingGetSQLInputContext ctx) throws SQLException { + return delegatingConverter.from(delegatingBinding.get0(ctx)); + } + @Override + final int sqltype(Configuration configuration) throws SQLException { + return delegatingBinding.sqltype(configuration); + } + } + static final class DefaultArrayBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 6875984626674331432L; + DefaultArrayBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @SuppressWarnings("unchecked") + @Override + final void sqlInline0(BindingSQLContext ctx, Object[] value) throws SQLException { + String separator = ""; + // H2 renders arrays as rows + if (ctx.family() == H2) { + ctx.render().sql('('); - // Most dialects implement SQL standard time literals - else { - render.visit(K_TIME).sql(" '").sql(escape(time, render)).sql('\''); + for (Object o : value) { + ctx.render().sql(separator); + binding((Class) type.getComponentType(), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), o)); + separator = ", "; } + + ctx.render().sql(')'); } + else if (ctx.family() == POSTGRES) { + ctx.render().visit(cast(inline(PostgresUtils.toPGArrayString(value)), type)); + } - else if (type == OffsetDateTime.class) { + // By default, render HSQLDB / POSTGRES syntax + else { + ctx.render().visit(K_ARRAY); + ctx.render().sql('['); - // [#5806] H2 doesn't support TIMESTAMP WITH TIME ZONE literals, see - if (family == H2) { - render.visit(K_CAST).sql("('").sql(escape(format((OffsetDateTime) val), render)).sql("' ") - .visit(K_AS).sql(' ').visit(K_TIMESTAMP_WITH_TIME_ZONE).sql(')'); + for (Object o : value) { + ctx.render().sql(separator); + binding((Class) type.getComponentType(), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), o)); + separator = ", "; } + ctx.render().sql(']'); + // [#3214] Some PostgreSQL array type literals need explicit casting + if (ctx.family() == POSTGRES && EnumType.class.isAssignableFrom(type.getComponentType())) + DefaultEnumTypeBinding.pgRenderEnumCast(ctx.render(), type); + } + } + @Override + final void sqlBind0(BindingSQLContext ctx, Object[] value) throws SQLException { + super.sqlBind0(ctx, value); + // In Postgres, some additional casting must be done in some cases... + if (ctx.family() == SQLDialect.POSTGRES) { + // Postgres needs explicit casting for enum (array) types + if (EnumType.class.isAssignableFrom(type.getComponentType())) + DefaultEnumTypeBinding.pgRenderEnumCast(ctx.render(), type); + // ... and also for other array types + else + ctx.render().sql("::") + .sql(DefaultDataType.getDataType(ctx.family(), type).getCastTypeName(ctx.render().configuration())); + } + } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + final void set0(BindingSetStatementContext ctx, Object[] value) throws SQLException { + switch (ctx.family()) { + case POSTGRES: { + ctx.statement().setString(ctx.index(), toPGArrayString(value)); + break; + } + case HSQLDB: { + Object[] a = value; + Class t = type; + // [#2325] [#5823] Cannot bind UUID[] type in HSQLDB. + // See also: https://sourceforge.net/p/hsqldb/bugs/1466 + if (t == UUID[].class) { + a = Convert.convertArray(a, byte[][].class); + t = byte[][].class; + } - - - - - // Some dialects implement SQL standard time literals - else { - render.visit(K_TIMESTAMP_WITH_TIME_ZONE).sql(" '").sql(escape(format((OffsetDateTime) val), render)).sql('\''); + ctx.statement().setArray(ctx.index(), new MockArray(ctx.family(), a, t)); + break; + } + case H2: { + ctx.statement().setObject(ctx.index(), value); + break; } + default: + throw new SQLDialectNotSupportedException("Cannot bind ARRAY types in dialect " + ctx.family()); } + } - else if (type == OffsetTime.class) { - String string = format((OffsetTime) val); - - - - + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + final void set0(BindingSetSQLOutputContext ctx, Object[] value) throws SQLException { + ctx.output().writeArray(new MockArray(ctx.family(), value, type())); + } + @Override + final Object[] get0(BindingGetResultSetContext ctx) throws SQLException { + switch (ctx.family()) { + case POSTGRES: + return pgGetArray(ctx, ctx.resultSet(), type, ctx.index()); + default: + // Note: due to a HSQLDB bug, it is not recommended to call rs.getObject() here: + // See https://sourceforge.net/tracker/?func=detail&aid=3181365&group_id=23316&atid=378131 + return convertArray(ctx.resultSet().getArray(ctx.index()), type()); + } + } + @Override + final Object[] get0(BindingGetStatementContext ctx) throws SQLException { + return convertArray(ctx.statement().getObject(ctx.index()), type()); + } + @Override + final Object[] get0(BindingGetSQLInputContext ctx) throws SQLException { + Array array = ctx.input().readArray(); + return array == null ? null : (Object[]) array.getArray(); + } - // Some dialects implement SQL standard time literals - { - render.visit(K_TIME_WITH_TIME_ZONE).sql(" '").sql(escape(string, render)).sql('\''); - } - } + @Override + final int sqltype(Configuration configuration) { + return Types.ARRAY; + } + /** + * Workarounds for the unimplemented Postgres JDBC driver features + */ + @SuppressWarnings("unchecked") + private static final T pgGetArray(Scope ctx, ResultSet rs, Class type, int index) throws SQLException { - else if (type.isArray()) { - String separator = ""; + // Get the JDBC Array and check for null. If null, that's OK + Array array = null; + try { - // H2 renders arrays as rows - if (family == H2) { - render.sql('('); + array = rs.getArray(index); + if (array == null) { + return null; + } - for (Object o : ((Object[]) val)) { - render.sql(separator); - new DefaultBinding(Converters.identity((Class) type.getComponentType()), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), o)); - separator = ", "; - } + // Try fetching a Java Object[]. That's gonna work for non-UDT types + try { - render.sql(')'); + // [#5633] Special treatment for this type. + // [#5586] [#5613] TODO: Improve PostgreSQL array deserialisation. + if (byte[][].class == type) + throw new ControlFlowSignal("GOTO the next array deserialisation strategy"); + else + return (T) convertArray(array, (Class) type); } - else if (family == POSTGRES) { - render.visit(cast(inline(PostgresUtils.toPGArrayString((Object[]) val)), type)); - } + // This might be a UDT (not implemented exception...) + catch (Exception e) { + List result = new ArrayList(); + ResultSet arrayRs = null; - // By default, render HSQLDB / POSTGRES syntax - else { - render.visit(K_ARRAY); - render.sql('['); + // Try fetching the array as a JDBC ResultSet + try { + arrayRs = array.getResultSet(); - for (Object o : ((Object[]) val)) { - render.sql(separator); - new DefaultBinding(Converters.identity((Class) type.getComponentType()), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), o)); - separator = ", "; + while (arrayRs.next()) { + binding((Class) type.getComponentType(), false).get(new DefaultBindingGetResultSetContext(ctx.configuration(), ctx.data(), arrayRs, 2)); + result.add(new DefaultBindingGetResultSetContext(ctx.configuration(), ctx.data(), arrayRs, 2).value()); + } } - render.sql(']'); + // That might fail too, then we don't know any further... + catch (Exception fatal) { + String string = null; + try { + string = rs.getString(index); + } + catch (SQLException ignore) {} + + log.error("Cannot parse array", string, fatal); + return null; + } - // [#3214] Some PostgreSQL array type literals need explicit casting - if (family == POSTGRES && EnumType.class.isAssignableFrom(type.getComponentType())) { - pgRenderEnumCast(render, type); + finally { + safeClose(arrayRs); } + + return (T) convertArray(result.toArray(), (Class) type); } } + finally { + safeFree(array); + } + } + private static final Object[] convertArray(Object array, Class type) throws SQLException { + if (array instanceof Object[]) + return Convert.convert(array, type); + else if (array instanceof Array) + return convertArray((Array) array, type); + return null; + } + private static final Object[] convertArray(Array array, Class type) throws SQLException { + if (array != null) + return Convert.convert(array.getArray(), type); - else if (EnumType.class.isAssignableFrom(type)) { - String literal = ((EnumType) val).getLiteral(); + return null; + } + } - if (literal == null) { - new DefaultBinding(Converters.identity((Class) String.class), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), literal)); - } - else { - new DefaultBinding(Converters.identity((Class) String.class), isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), literal)); - } - } - else if (UDTRecord.class.isAssignableFrom(type)) { - render.sql("[UDT]"); - } - // Known fall-through types: - // - Blob, Clob (both not supported by jOOQ) - // - String - // - UUID - else { - render.sql('\'') - .sql(escape(val, render), true) - .sql('\''); - } - } - // In Postgres, some additional casting must be done in some cases... - else if (family == SQLDialect.POSTGRES) { - // Postgres needs explicit casting for enum (array) types - if (EnumType.class.isAssignableFrom(type) || - (type.isArray() && EnumType.class.isAssignableFrom(type.getComponentType()))) { - render.sql(ctx.variable()); - pgRenderEnumCast(render, type); - } - // ... and also for other array types - else if (type.isArray() && byte[].class != type) { - render.sql(ctx.variable()); - render.sql("::"); - render.sql(DefaultDataType.getDataType(family, type).getCastTypeName(render.configuration())); - } - else { - render.sql(ctx.variable()); - } - } @@ -830,73 +1114,17 @@ else if (type.isArray() && byte[].class != type) { - else { - render.sql(ctx.variable()); - } - } - private static final Time getTime(Class t, Object val) { - return t == Time.class ? (Time) val : Time.valueOf((LocalTime) val) ; - } - private static final Timestamp getTimestamp(Class t, Object val) { - return t == Timestamp.class ? (Timestamp) val : Timestamp.valueOf((LocalDateTime) val) ; - } - private static final Date getDate(Class t, Object val) { - return t == Date.class ? (Date) val : Date.valueOf((LocalDate) val) ; - } - /** - * Escape a string literal by replacing ' by '', and possibly also backslashes. - */ - private static final String escape(Object val, Context context) { - String result = val.toString(); - if (needsBackslashEscaping(context.configuration())) - result = result.replace("\\", "\\\\"); - return result.replace("'", "''"); - } - /** - * Convert a byte array to a hex encoded string. - * - * @param value the byte array - * @return the hex encoded string - */ - private static final String convertBytesToHex(byte[] value) { - return convertBytesToHex(value, value.length); - } - /** - * Convert a byte array to a hex encoded string. - * - * @param value the byte array - * @param len the number of bytes to encode - * @return the hex encoded string - */ - private static final String convertBytesToHex(byte[] value, int len) { - char[] buff = new char[len + len]; - char[] hex = HEX; - for (int i = 0; i < len; i++) { - int c = value[i] & 0xff; - buff[i + i] = hex[c >> 4]; - buff[i + i + 1] = hex[c & 0xf]; - } - return new String(buff); - } - @SuppressWarnings("unchecked") - @Override - public void register(BindingRegisterContext ctx) throws SQLException { - Configuration configuration = ctx.configuration(); - int sqlType = DefaultDataType.getDataType(ctx.dialect(), type).getSQLType(ctx.configuration()); - if (log.isTraceEnabled()) - log.trace("Registering variable " + ctx.index(), "" + type); - switch (configuration.family()) { @@ -931,30 +1159,9 @@ public void register(BindingRegisterContext ctx) throws SQLException { - default: { - ctx.statement().registerOutParameter(ctx.index(), sqlType); - break; - } - } - } - @SuppressWarnings("unchecked") - @Override - public void set(BindingSetStatementContext ctx) throws SQLException { - Configuration configuration = ctx.configuration(); - SQLDialect dialect = ctx.dialect(); - T value = converter.to(ctx.value()); - if (log.isTraceEnabled()) - if (value != null && value.getClass().isArray() && value.getClass() != byte[].class) - log.trace("Binding variable " + ctx.index(), Arrays.asList((Object[]) value) + " (" + type + ")"); - else - log.trace("Binding variable " + ctx.index(), value + " (" + type + ")"); - // Setting null onto a prepared statement is subtly different for every - // SQL dialect. See the following section for details - if (value == null) { - int sqlType = DefaultDataType.getDataType(dialect, type).getSQLType(configuration); @@ -965,146 +1172,294 @@ public void set(BindingSetStatementContext ctx) throws SQLException { - // [#1126] Oracle's UDTs need to be bound with their type name - if (UDTRecord.class.isAssignableFrom(type)) { - ctx.statement().setNull(ctx.index(), sqlType, Tools.getMappedUDTName(configuration, (Class>) type)); + + + default: + return Types.ARRAY; } + } - // [#1225] [#1227] TODO Put this logic into DataType - // Some dialects have trouble binding binary data as BLOB - else if (asList(POSTGRES).contains(configuration.family()) && sqlType == Types.BLOB) { - ctx.statement().setNull(ctx.index(), Types.BINARY); + @SuppressWarnings("unchecked") + static final ArrayRecord set(Configuration configuration, ArrayRecord target, Array source) throws SQLException { + if (source == null) { + target.clear(); } + else { + // [#1179 #1376 #1377] This is needed to load TABLE OF OBJECT + // [#884] TODO: This name is used in inlined SQL. It should be + // correctly escaped and schema mapped! + Object[] array = (Object[]) source.getArray(typeMap(target.getClass(), configuration)); + T[] converted = (T[]) new Object[array.length]; + for (int i = 0; i < array.length; i++) + // [#5404] Recurse for nested arrays + if (array[i] instanceof Array) + converted[i] = (T) set(configuration, (ArrayRecord) on(target.getDataType().getType()).create().get(), (Array) array[i]); + else + converted[i] = target.getDataType().convert(array[i]); + target.addAll(Arrays.asList(converted)); + } + // [#4372] Attach records if possible / required + if (attachRecords(configuration)) + target.attach(configuration); + return target; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final ArrayRecord getArrayRecord(Configuration configuration, Array array, Class> type) throws SQLException { + if (array == null) { + return null; + } + else { + // TODO: [#523] Use array record meta data instead + // ... Generic type inference in Java 8 has changed, resorting to + // raw type for now + return set(configuration, (ArrayRecord) Tools.newArrayRecord(type), array); + } + } + private static final Array createOracleARRAY(Configuration configuration, ArrayRecord array) { + // [#1544] We can safely assume that localConfiguration has been + // set on DefaultBindContext, prior to serialising arrays to SQLOutput + Array result = on(localTargetConnection()).call("createARRAY", Tools.getMappedArrayNameString(configuration, array), unnest(array)).get(); + DefaultExecuteContext.register(result); + return result; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final ResultSet createSQLServerTable(Configuration configuration, ArrayRecord array) { + UDTRecord udt = Tools.newRecord(false, (Class>) array.getDataType().getType()).operate(null); + Result r = DSL.using(configuration).newResult(udt.getUDT().fields()); + r.addAll((List) array); + return new MockResultSet(r); + } + @SuppressWarnings("unchecked") + private static final Object[] unnest(ArrayRecord array) { + if (array == null) + return null; + Object[] result = array.toArray(); + // [#5404] Recurse for nested arrays + if (result != null) + for (int i = 0; i < result.length; i++) + if (result[i] instanceof ArrayRecord) + result[i] = unnest((ArrayRecord) result[i]); + else + result[i] = ((DataType) array.getDataType()).getConverter().to(result[i]); + return result; + } + } + /* [/pro] */ + static final class DefaultBigDecimalBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -8912971184035434281L; + DefaultBigDecimalBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, BigDecimal value) { + ctx.render().sql(value.toString()); + } + @Override + final void set0(BindingSetStatementContext ctx, BigDecimal value) throws SQLException { + if (asList(SQLITE).contains(ctx.family())) + ctx.statement().setString(ctx.index(), value.toString()); + else + ctx.statement().setBigDecimal(ctx.index(), value); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, BigDecimal value) throws SQLException { + ctx.output().writeBigDecimal(value); + } + @Override + final BigDecimal get0(BindingGetResultSetContext ctx) throws SQLException { + // The SQLite JDBC driver doesn't support BigDecimals + if (ctx.family() == SQLDialect.SQLITE) + return Convert.convert(ctx.resultSet().getString(ctx.index()), BigDecimal.class); + else + return ctx.resultSet().getBigDecimal(ctx.index()); + } + @Override + final BigDecimal get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getBigDecimal(ctx.index()); + } + @Override + final BigDecimal get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readBigDecimal(); + } + @Override + final int sqltype(Configuration configuration) { + return Types.DECIMAL; + } + } + static final class DefaultBigIntegerBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -3857031689661809421L; + DefaultBigIntegerBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } - // All other types can be set to null if the JDBC type is known - else if (sqlType != Types.OTHER) { - ctx.statement().setNull(ctx.index(), sqlType); - } + @Override + final void sqlInline0(BindingSQLContext ctx, BigInteger value) { + ctx.render().sql(value.toString()); + } + @Override + final void set0(BindingSetStatementContext ctx, BigInteger value) throws SQLException { + if (asList(SQLITE).contains(ctx.family())) + ctx.statement().setString(ctx.index(), value.toString()); + else + ctx.statement().setBigDecimal(ctx.index(), new BigDecimal(value)); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, BigInteger value) throws SQLException { + ctx.output().writeBigDecimal(new BigDecimal(value)); + } + @Override + final BigInteger get0(BindingGetResultSetContext ctx) throws SQLException { + // The SQLite JDBC driver doesn't support BigDecimals + if (ctx.configuration().dialect() == SQLDialect.SQLITE) + return Convert.convert(ctx.resultSet().getString(ctx.index()), BigInteger.class); + BigDecimal b = ctx.resultSet().getBigDecimal(ctx.index()); + return (b == null ? null : b.toBigInteger()); + } + @Override + final BigInteger get0(BindingGetStatementContext ctx) throws SQLException { + BigDecimal d = ctx.statement().getBigDecimal(ctx.index()); + return (d == null ? null : d.toBigInteger()); + } + @Override + final BigInteger get0(BindingGetSQLInputContext ctx) throws SQLException { + BigDecimal d = ctx.input().readBigDecimal(); + return (d == null ? null : d.toBigInteger()); + } + @Override + final int sqltype(Configuration configuration) { + return Types.DECIMAL; + } + } + static final class DefaultBlobBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -4605427469676162501L; + DefaultBlobBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void set0(BindingSetStatementContext ctx, Blob value) throws SQLException { + ctx.statement().setBlob(ctx.index(), value); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, Blob value) throws SQLException { + ctx.output().writeBlob(value); + } - // [#729] In the absence of the correct JDBC type, try setObject - else { - ctx.statement().setObject(ctx.index(), null); - } + @Override + final Blob get0(BindingGetResultSetContext ctx) throws SQLException { + return ctx.resultSet().getBlob(ctx.index()); } - else { - Class actualType = type; - // Try to infer the bind value type from the actual bind value if possible. - if (actualType == Object.class) { - actualType = value.getClass(); - } + @Override + final Blob get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getBlob(ctx.index()); + } - if (actualType == Blob.class) { - ctx.statement().setBlob(ctx.index(), (Blob) value); - } - else if (actualType == Boolean.class) { + @Override + final Blob get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readBlob(); + } + @Override + final int sqltype(Configuration configuration) { + switch (configuration.family()) { + // [#1225] [#1227] TODO Put this logic into DataType + // Some dialects have trouble binding binary data as BLOB + // Same logic in DefaultBytesBinding + case POSTGRES: + return Types.BINARY; - ctx.statement().setBoolean(ctx.index(), (Boolean) value); - } - else if (actualType == BigDecimal.class) { - if (asList(SQLITE).contains(dialect.family())) { - ctx.statement().setString(ctx.index(), value.toString()); - } - else { - ctx.statement().setBigDecimal(ctx.index(), (BigDecimal) value); - } - } - else if (actualType == BigInteger.class) { - if (asList(SQLITE).contains(dialect.family())) { - ctx.statement().setString(ctx.index(), value.toString()); - } - else { - ctx.statement().setBigDecimal(ctx.index(), new BigDecimal((BigInteger) value)); - } - } - else if (actualType == Byte.class) { - ctx.statement().setByte(ctx.index(), (Byte) value); - } - else if (actualType == byte[].class) { - ctx.statement().setBytes(ctx.index(), (byte[]) value); - } - else if (actualType == Clob.class) { - ctx.statement().setClob(ctx.index(), (Clob) value); - } - else if (actualType == Double.class) { - ctx.statement().setDouble(ctx.index(), (Double) value); - } - else if (actualType == Float.class) { - ctx.statement().setFloat(ctx.index(), (Float) value); - } - else if (actualType == Integer.class) { - ctx.statement().setInt(ctx.index(), (Integer) value); + default: + return Types.BLOB; } - else if (actualType == Long.class) { + } + } + static final class DefaultBooleanBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -533762957890251203L; + DefaultBooleanBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, Boolean value) { - ctx.statement().setLong(ctx.index(), (Long) value); - } - else if (actualType == Short.class) { - ctx.statement().setShort(ctx.index(), (Short) value); + // [#1153] Some dialects don't support boolean literals TRUE and FALSE + if (asList(FIREBIRD, SQLITE).contains(ctx.family())) { + ctx.render().sql(value ? "1" : "0"); } - else if (actualType == String.class) { - ctx.statement().setString(ctx.index(), (String) value); + + + + + + else { + ctx.render().visit(value ? K_TRUE : K_FALSE); } + } - // There is potential for trouble when binding date time as such - // ------------------------------------------------------------- - else if (Tools.isDate(actualType)) { - Date date = getDate(actualType, value); + @Override + final void register0(BindingRegisterContext ctx) throws SQLException { - if (dialect == SQLITE) { - ctx.statement().setString(ctx.index(), date.toString()); - } @@ -1113,34 +1468,14 @@ else if (Tools.isDate(actualType)) { - else { - ctx.statement().setDate(ctx.index(), date); - } - } - else if (Tools.isTime(actualType)) { - Time time = getTime(actualType, value); + super.register0(ctx); + } - if (dialect == SQLITE) { - ctx.statement().setString(ctx.index(), time.toString()); - } - else { - ctx.statement().setTime(ctx.index(), time); - } - } - else if (Tools.isTimestamp(actualType)) { - Timestamp timestamp = getTimestamp(actualType, value); + @Override + final void set0(BindingSetStatementContext ctx, Boolean value) throws SQLException { - if (dialect == SQLITE) { - ctx.statement().setString(ctx.index(), timestamp.toString()); - } - else { - ctx.statement().setTimestamp(ctx.index(), timestamp); - } - } - else if (actualType == OffsetTime.class) { - String string = format((OffsetTime) value); @@ -1148,115 +1483,110 @@ else if (actualType == OffsetTime.class) { - ctx.statement().setString(ctx.index(), string); - } - else if (actualType == OffsetDateTime.class) { - ctx.statement().setString(ctx.index(), format((OffsetDateTime) value)); - } + ctx.statement().setBoolean(ctx.index(), value); + } + @Override + final void setNull0(BindingSetStatementContext ctx) throws SQLException { - // [#566] Interval data types are best bound as Strings - else if (actualType == YearToMonth.class) { - if (dialect.family() == POSTGRES) { - ctx.statement().setObject(ctx.index(), toPGInterval((YearToMonth) value)); - } - else { - ctx.statement().setString(ctx.index(), value.toString()); - } - } - else if (actualType == DayToSecond.class) { - if (dialect.family() == POSTGRES) { - ctx.statement().setObject(ctx.index(), toPGInterval((DayToSecond) value)); - } - else { - ctx.statement().setString(ctx.index(), value.toString()); - } - } - else if (actualType == UByte.class) { - ctx.statement().setShort(ctx.index(), ((UByte) value).shortValue()); - } - else if (actualType == UShort.class) { - ctx.statement().setInt(ctx.index(), ((UShort) value).intValue()); - } - else if (actualType == UInteger.class) { - ctx.statement().setLong(ctx.index(), ((UInteger) value).longValue()); - } - else if (actualType == ULong.class) { + super.setNull0(ctx); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, Boolean value) throws SQLException { + ctx.output().writeBoolean(value); + } - ctx.statement().setBigDecimal(ctx.index(), new BigDecimal(value.toString())); - } - else if (actualType == UUID.class) { - switch (dialect.family()) { - - // [#1624] Some JDBC drivers natively support the - // java.util.UUID data type - case H2: - case POSTGRES: { - ctx.statement().setObject(ctx.index(), value); - break; - } + @Override + final Boolean get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Boolean.valueOf(ctx.resultSet().getBoolean(ctx.index()))); + } + @Override + final Boolean get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Boolean.valueOf(ctx.statement().getBoolean(ctx.index()))); + } + @Override + final Boolean get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Boolean.valueOf(ctx.input().readBoolean())); + } + @Override + final int sqltype(Configuration configuration) { + return Types.BOOLEAN; + } + } + static final class DefaultByteBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -7328193812163714614L; + DefaultByteBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, Byte value) { + ctx.render().sql(value.toString()); + } - // Most databases don't have such a type. In this case, jOOQ - // emulates the type - default: { - ctx.statement().setString(ctx.index(), value.toString()); - break; - } - } - } + @Override + final void set0(BindingSetStatementContext ctx, Byte value) throws SQLException { + ctx.statement().setByte(ctx.index(), value); + } - // The type byte[] is handled earlier. byte[][] can be handled here - else if (actualType.isArray()) { - switch (dialect.family()) { - case POSTGRES: { - ctx.statement().setString(ctx.index(), toPGArrayString((Object[]) value)); - break; - } - case HSQLDB: { - Object[] a = (Object[]) value; - Class t = actualType; - - // [#2325] [#5823] Cannot bind UUID[] type in HSQLDB. - // See also: https://sourceforge.net/p/hsqldb/bugs/1466 - if (actualType == UUID[].class) { - a = Convert.convertArray(a, byte[][].class); - t = byte[][].class; - } + @Override + final void set0(BindingSetSQLOutputContext ctx, Byte value) throws SQLException { + ctx.output().writeByte(value); + } - ctx.statement().setArray(ctx.index(), new MockArray(dialect, a, t)); - break; - } - case H2: { - ctx.statement().setObject(ctx.index(), value); - break; - } - default: - throw new SQLDialectNotSupportedException("Cannot bind ARRAY types in dialect " + dialect); - } - } + @Override + final Byte get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Byte.valueOf(ctx.resultSet().getByte(ctx.index()))); + } + @Override + final Byte get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Byte.valueOf(ctx.statement().getByte(ctx.index()))); + } + @Override + final Byte get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Byte.valueOf(ctx.input().readByte())); + } + @Override + final int sqltype(Configuration configuration) { + return Types.TINYINT; + } + } + static final class DefaultBytesBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -727202499908007757L; + DefaultBytesBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, byte[] value) { + // [#1154] Binary data cannot always be inlined @@ -1270,70 +1600,40 @@ else if (actualType.isArray()) { + if (asList(DERBY, H2, HSQLDB, MARIADB, MYSQL, SQLITE).contains(ctx.family())) + ctx.render() + .sql("X'") + .sql(convertBytesToHex(value)) + .sql('\''); - else if (EnumType.class.isAssignableFrom(actualType)) { - ctx.statement().setString(ctx.index(), ((EnumType) value).getLiteral()); - } - else { - ctx.statement().setObject(ctx.index(), value); - } - } - } - @Override - public void set(BindingSetSQLOutputContext ctx) throws SQLException { - Configuration configuration = ctx.configuration(); - T value = converter.to(ctx.value()); - if (value == null) { - ctx.output().writeObject(null); - } - else if (type == Blob.class) { - ctx.output().writeBlob((Blob) value); - } - else if (type == Boolean.class) { - ctx.output().writeBoolean((Boolean) value); - } - else if (type == BigInteger.class) { - ctx.output().writeBigDecimal(new BigDecimal((BigInteger) value)); - } - else if (type == BigDecimal.class) { - ctx.output().writeBigDecimal((BigDecimal) value); - } - else if (type == Byte.class) { - ctx.output().writeByte((Byte) value); - } - else if (type == byte[].class) { - // [#1327] Oracle cannot serialise BLOBs as byte[] to SQLOutput - // Use reflection to avoid dependency on OJDBC - if (isLob) { - Blob blob = null; - try { - blob = on("oracle.sql.BLOB").call("createTemporary", - on(ctx.output()).call("getSTRUCT") - .call("getJavaSqlConnection").get(), - false, - on("oracle.sql.BLOB").get("DURATION_SESSION")).get(); - - blob.setBytes(1, (byte[]) value); - ctx.output().writeBlob(blob); - } - finally { - DefaultExecuteContext.register(blob); - } - } - else { - ctx.output().writeBytes((byte[]) value); - } + + + else if (ctx.family() == POSTGRES) + ctx.render() + .sql("E'") + .sql(PostgresUtils.toPGString(value)) + .sql("'::bytea"); + + // This default behaviour is used in debug logging for dialects + // that do not support inlining binary data + else + ctx.render() + .sql("X'") + .sql(convertBytesToHex(value)) + .sql('\''); } - else if (type == Clob.class) { - ctx.output().writeClob((Clob) value); + + @Override + final void set0(BindingSetStatementContext ctx, byte[] value) throws SQLException { + ctx.statement().setBytes(ctx.index(), value); } - else if (type == Date.class) { - Date date = (Date) value; + @Override + final void set0(BindingSetSQLOutputContext ctx, byte[] value) throws SQLException { @@ -1342,85 +1642,210 @@ else if (type == Date.class) { - ctx.output().writeDate(date); - } - else if (type == Double.class) { - ctx.output().writeDouble((Double) value); - } - else if (type == Float.class) { - ctx.output().writeFloat((Float) value); - } - else if (type == Integer.class) { - ctx.output().writeInt((Integer) value); - } - else if (type == Long.class) { - ctx.output().writeLong((Long) value); - } - else if (type == Short.class) { - ctx.output().writeShort((Short) value); - } - else if (type == String.class) { - // [#1327] Oracle cannot serialise CLOBs as String to SQLOutput - // Use reflection to avoid dependency on OJDBC - if (isLob) { - Clob clob = null; - try { - clob = on("oracle.sql.CLOB").call("createTemporary", - on(ctx.output()).call("getSTRUCT") - .call("getJavaSqlConnection").get(), - false, - on("oracle.sql.CLOB").get("DURATION_SESSION")).get(); - - clob.setString(1, (String) value); - ctx.output().writeClob(clob); - } - finally { - DefaultExecuteContext.register(clob); - } - } - else { - ctx.output().writeString((String) value); + + + + + + + + + + + + + + + ctx.output().writeBytes(value); + } + + @Override + final byte[] get0(BindingGetResultSetContext ctx) throws SQLException { + return ctx.resultSet().getBytes(ctx.index()); + } + + @Override + final byte[] get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getBytes(ctx.index()); + } + + @Override + final byte[] get0(BindingGetSQLInputContext ctx) throws SQLException { + + + + + + + + + + + + + + + + + return ctx.input().readBytes(); + } + + @Override + final int sqltype(Configuration configuration) { + switch (configuration.family()) { + // [#1225] [#1227] TODO Put this logic into DataType + // Some dialects have trouble binding binary data as BLOB + // Same logic in DefaultBlobBinding + + case POSTGRES: + + + + + + return Types.BINARY; + + default: + return Types.BLOB; } } - else if (type == Time.class) { - ctx.output().writeTime((Time) value); + } + + static final class DefaultClobBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = -3890447233590678873L; + + DefaultClobBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == Timestamp.class) { - ctx.output().writeTimestamp((Timestamp) value); + + @Override + final void set0(BindingSetStatementContext ctx, Clob value) throws SQLException { + ctx.statement().setClob(ctx.index(), value); } - else if (type == YearToMonth.class) { - ctx.output().writeString(value.toString()); + + @Override + final void set0(BindingSetSQLOutputContext ctx, Clob value) throws SQLException { + ctx.output().writeClob(value); } - else if (type == DayToSecond.class) { - ctx.output().writeString(value.toString()); + + @Override + final Clob get0(BindingGetResultSetContext ctx) throws SQLException { + return ctx.resultSet().getClob(ctx.index()); } -// else if (type.isArray()) { -// stream.writeArray(value); -// } - else if (UNumber.class.isAssignableFrom(type)) { - ctx.output().writeString(value.toString()); + + @Override + final Clob get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getClob(ctx.index()); } - else if (type == UUID.class) { - ctx.output().writeString(value.toString()); + + @Override + final Clob get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readClob(); + } + + @Override + final int sqltype(Configuration configuration) { + return Types.CLOB; + } + } + + static final class DefaultDateBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = -4797649501187223237L; + + DefaultDateBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + + @Override + final void sqlInline0(BindingSQLContext ctx, Date value) { + // [#1156] DATE / TIME inlining is very vendor-specific + + // The SQLite JDBC driver does not implement the escape syntax + // [#1253] Sybase does not implement date literals + if (asList(SQLITE).contains(ctx.family())) + ctx.render().sql('\'').sql(escape(value, ctx.render())).sql('\''); + + + + + + + + + + + + + + // [#1253] Derby doesn't support the standard literal + else if (ctx.family() == DERBY) + ctx.render().visit(K_DATE).sql("('").sql(escape(value, ctx.render())).sql("')"); + + // [#3648] Circumvent a MySQL bug related to date literals + else if (ctx.family() == MYSQL) + ctx.render().sql("{d '").sql(escape(value, ctx.render())).sql("'}"); + + // Most dialects implement SQL standard date literals + else + ctx.render().visit(K_DATE).sql(" '").sql(escape(value, ctx.render())).sql('\''); } + @Override + final void sqlBind0(BindingSQLContext ctx, Date value) throws SQLException { + + + + + + - else if (EnumType.class.isAssignableFrom(type)) { - ctx.output().writeString(((EnumType) value).getLiteral()); + + super.sqlBind0(ctx, value); } - else if (UDTRecord.class.isAssignableFrom(type)) { - ctx.output().writeObject((UDTRecord) value); + + @Override + final void register0(BindingRegisterContext ctx) throws SQLException { + + + + + + + + + super.register0(ctx); } - else { - throw new UnsupportedOperationException("Type " + type + " is not supported"); + + @Override + final void set0(BindingSetStatementContext ctx, Date value) throws SQLException { + if (ctx.family() == SQLITE) + ctx.statement().setString(ctx.index(), value.toString()); + + + + + + + + else + ctx.statement().setDate(ctx.index(), value); } - } + @Override + final void set0(BindingSetSQLOutputContext ctx, Date value) throws SQLException { @@ -1429,7 +1854,18 @@ else if (UDTRecord.class.isAssignableFrom(type)) { + ctx.output().writeDate(value); + } + + @Override + final Date get0(BindingGetResultSetContext ctx) throws SQLException { + SQLDialect family = ctx.family(); + // SQLite's type affinity needs special care... + if (family == SQLDialect.SQLITE) { + String date = ctx.resultSet().getString(ctx.index()); + return date == null ? null : new Date(parse(Date.class, date)); + } @@ -1439,7 +1875,13 @@ else if (UDTRecord.class.isAssignableFrom(type)) { + else { + return ctx.resultSet().getDate(ctx.index()); + } + } + @Override + final Date get0(BindingGetStatementContext ctx) throws SQLException { @@ -1450,7 +1892,11 @@ else if (UDTRecord.class.isAssignableFrom(type)) { + return ctx.statement().getDate(ctx.index()); + } + @Override + final Date get0(BindingGetSQLInputContext ctx) throws SQLException { @@ -1459,211 +1905,391 @@ else if (UDTRecord.class.isAssignableFrom(type)) { - @SuppressWarnings("unchecked") - @Override - public void get(BindingGetResultSetContext ctx) throws SQLException { - T result = null; - if (type == Blob.class) { - result = (T) ctx.resultSet().getBlob(ctx.index()); + + return ctx.input().readDate(); } - else if (type == Boolean.class) { - result = (T) wasNull(ctx.resultSet(), Boolean.valueOf(ctx.resultSet().getBoolean(ctx.index()))); + + @Override + final int sqltype(Configuration configuration) { + return Types.DATE; } - else if (type == BigInteger.class) { - // The SQLite JDBC driver doesn't support BigDecimals - if (ctx.configuration().dialect() == SQLDialect.SQLITE) { - result = Convert.convert(ctx.resultSet().getString(ctx.index()), (Class) BigInteger.class); + } + + static final class DefaultDayToSecondBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 4378118707359663541L; + + DefaultDayToSecondBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + + @Override + final void set0(BindingSetStatementContext ctx, DayToSecond value) throws SQLException { + + // [#566] Interval data types are best bound as Strings + if (ctx.family() == POSTGRES) + ctx.statement().setObject(ctx.index(), toPGInterval(value)); + else + ctx.statement().setString(ctx.index(), value.toString()); + } + + @Override + final void set0(BindingSetSQLOutputContext ctx, DayToSecond value) throws SQLException { + ctx.output().writeString(value.toString()); + } + + @Override + final DayToSecond get0(BindingGetResultSetContext ctx) throws SQLException { + if (ctx.family() == POSTGRES) { + Object object = ctx.resultSet().getObject(ctx.index()); + return object == null ? null : PostgresUtils.toDayToSecond(object); } else { - BigDecimal b = ctx.resultSet().getBigDecimal(ctx.index()); - result = (T) (b == null ? null : b.toBigInteger()); + String string = ctx.resultSet().getString(ctx.index()); + return string == null ? null : DayToSecond.valueOf(string); } } - else if (type == BigDecimal.class) { - // The SQLite JDBC driver doesn't support BigDecimals - if (ctx.configuration().dialect() == SQLDialect.SQLITE) { - result = Convert.convert(ctx.resultSet().getString(ctx.index()), (Class) BigDecimal.class); + + @Override + final DayToSecond get0(BindingGetStatementContext ctx) throws SQLException { + if (ctx.family() == POSTGRES) { + Object object = ctx.statement().getObject(ctx.index()); + return object == null ? null : PostgresUtils.toDayToSecond(object); } else { - result = (T) ctx.resultSet().getBigDecimal(ctx.index()); + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : DayToSecond.valueOf(string); } } - else if (type == Byte.class) { - result = (T) wasNull(ctx.resultSet(), Byte.valueOf(ctx.resultSet().getByte(ctx.index()))); + + @Override + final DayToSecond get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : DayToSecond.valueOf(string); + } + + @Override + final int sqltype(Configuration configuration) { + return Types.VARCHAR; + } + } + + static final class DefaultDoubleBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = -615723070592774059L; + + DefaultDoubleBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == byte[].class) { - result = (T) ctx.resultSet().getBytes(ctx.index()); + + @Override + final void sqlInline0(BindingSQLContext ctx, Double value) { + + // [#5249] Special inlining of special floating point values + if (value.isNaN() && ctx.family() == POSTGRES) + ctx.render().visit(inline("NaN")).sql("::float8"); + else + ctx.render().sql(value.toString()); } - else if (type == Clob.class) { - result = (T) ctx.resultSet().getClob(ctx.index()); + + @Override + final void set0(BindingSetStatementContext ctx, Double value) throws SQLException { + ctx.statement().setDouble(ctx.index(), value); } - else if (type == Date.class) { - result = (T) getDate(ctx.family(), ctx.resultSet(), ctx.index()); + + @Override + final void set0(BindingSetSQLOutputContext ctx, Double value) throws SQLException { + ctx.output().writeDouble(value); } - else if (type == Double.class) { - result = (T) wasNull(ctx.resultSet(), Double.valueOf(ctx.resultSet().getDouble(ctx.index()))); + + @Override + final Double get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Double.valueOf(ctx.resultSet().getDouble(ctx.index()))); } - else if (type == Float.class) { - result = (T) wasNull(ctx.resultSet(), Float.valueOf(ctx.resultSet().getFloat(ctx.index()))); + + @Override + final Double get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Double.valueOf(ctx.statement().getDouble(ctx.index()))); } - else if (type == Integer.class) { - result = (T) wasNull(ctx.resultSet(), Integer.valueOf(ctx.resultSet().getInt(ctx.index()))); + + @Override + final Double get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Double.valueOf(ctx.input().readDouble())); } - else if (type == LocalDate.class) { - result = (T) localDate(getDate(ctx.family(), ctx.resultSet(), ctx.index())); + @Override + final int sqltype(Configuration configuration) { + return Types.DOUBLE; } - else if (type == LocalTime.class) { - result = (T) localTime(getTime(ctx.family(), ctx.resultSet(), ctx.index())); + } + + static final class DefaultEnumTypeBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = -5858761464381778695L; + + DefaultEnumTypeBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == LocalDateTime.class) { - result = (T) localDateTime(getTimestamp(ctx.family(), ctx.resultSet(), ctx.index())); + + @Override + final void sqlInline0(BindingSQLContext ctx, EnumType value) throws SQLException { + String literal = value.getLiteral(); + + if (literal == null) + binding(String.class, isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), literal)); + else + binding(String.class, isLob).sql(new DefaultBindingSQLContext(ctx.configuration(), ctx.data(), ctx.render(), literal)); } - else if (type == Long.class) { - result = (T) wasNull(ctx.resultSet(), Long.valueOf(ctx.resultSet().getLong(ctx.index()))); + @Override + final void sqlBind0(BindingSQLContext ctx, EnumType value) throws SQLException { + super.sqlBind0(ctx, value); + + // Postgres needs explicit casting for enum (array) types + if (ctx.family() == SQLDialect.POSTGRES) + pgRenderEnumCast(ctx.render(), type); } - else if (type == OffsetTime.class) { - result = (T) offsetTime(ctx.resultSet().getString(ctx.index())); + @Override + final void set0(BindingSetStatementContext ctx, EnumType value) throws SQLException { + ctx.statement().setString(ctx.index(), value.getLiteral()); } - else if (type == OffsetDateTime.class) { - result = (T) offsetDateTime(ctx.resultSet().getString(ctx.index())); + + @Override + final void set0(BindingSetSQLOutputContext ctx, EnumType value) throws SQLException { + ctx.output().writeString(value.getLiteral()); } - else if (type == Short.class) { - result = (T) wasNull(ctx.resultSet(), Short.valueOf(ctx.resultSet().getShort(ctx.index()))); + @Override + final EnumType get0(BindingGetResultSetContext ctx) throws SQLException { + return getEnumType(type(), ctx.resultSet().getString(ctx.index())); } - else if (type == String.class) { - result = (T) ctx.resultSet().getString(ctx.index()); + + @Override + final EnumType get0(BindingGetStatementContext ctx) throws SQLException { + return getEnumType(type(), ctx.statement().getString(ctx.index())); } - else if (type == Time.class) { - result = (T) getTime(ctx.family(), ctx.resultSet(), ctx.index()); + + @Override + final EnumType get0(BindingGetSQLInputContext ctx) throws SQLException { + return getEnumType(type(), ctx.input().readString()); } - else if (type == Timestamp.class) { - result = (T) getTimestamp(ctx.family(), ctx.resultSet(), ctx.index()); + + @Override + final int sqltype(Configuration configuration) { + return Types.VARCHAR; } - else if (type == YearToMonth.class) { - if (ctx.family() == POSTGRES) { - Object object = ctx.resultSet().getObject(ctx.index()); - result = (T) (object == null ? null : PostgresUtils.toYearToMonth(object)); - } - else { - String string = ctx.resultSet().getString(ctx.index()); - result = (T) (string == null ? null : YearToMonth.valueOf(string)); + + static final void pgRenderEnumCast(RenderContext render, Class type) { + + @SuppressWarnings("unchecked") + Class enumType = (Class) ( + type.isArray() ? type.getComponentType() : type); + + // [#968] Don't cast "synthetic" enum types (note, val can be null!) + // [#4427] Java Enum agnostic implementation will work for Scala also + EnumType[] enums = Tools.enums(enumType); + + if (enums == null || enums.length == 0) + throw new IllegalArgumentException("Not a valid EnumType : " + type); + + Schema schema = enums[0].getSchema(); + if (schema != null) { + render.sql("::"); + + schema = using(render.configuration()).map(schema); + if (schema != null && TRUE.equals(render.configuration().settings().isRenderSchema())) { + render.visit(schema); + render.sql('.'); + } + + render.visit(name(enums[0].getName())); } + + if (type.isArray()) + render.sql("[]"); } - else if (type == DayToSecond.class) { - if (ctx.family() == POSTGRES) { - Object object = ctx.resultSet().getObject(ctx.index()); - result = (T) (object == null ? null : PostgresUtils.toDayToSecond(object)); + + @SuppressWarnings("unchecked") + static final E getEnumType(Class type, String literal) { + try { + EnumType[] list = Tools.enums(type); + + for (EnumType e : list) + if (e.getLiteral().equals(literal)) + return (E) e; } - else { - String string = ctx.resultSet().getString(ctx.index()); - result = (T) (string == null ? null : DayToSecond.valueOf(string)); + catch (Exception e) { + throw new DataTypeException("Unknown enum literal found : " + literal); } + + return null; } - else if (type == UByte.class) { - result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UByte.class); + } + + static final class DefaultFloatBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = -2468794191255859536L; + + DefaultFloatBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == UShort.class) { - result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UShort.class); + + @Override + final void sqlInline0(BindingSQLContext ctx, Float value) { + + // [#5249] Special inlining of special floating point values + if (value.isNaN() && POSTGRES == ctx.family()) + ctx.render().visit(inline("NaN")).sql("::float4"); + else + ctx.render().sql(value.toString()); } - else if (type == UInteger.class) { - result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UInteger.class); + + @Override + final void set0(BindingSetStatementContext ctx, Float value) throws SQLException { + ctx.statement().setFloat(ctx.index(), value); } - else if (type == ULong.class) { - result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), ULong.class); + + @Override + final void set0(BindingSetSQLOutputContext ctx, Float value) throws SQLException { + ctx.output().writeFloat(value); } - else if (type == UUID.class) { - switch (ctx.family()) { - // [#1624] Some JDBC drivers natively support the - // java.util.UUID data type - case H2: - case POSTGRES: { - result = (T) ctx.resultSet().getObject(ctx.index()); - break; - } + @Override + final Float get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Float.valueOf(ctx.resultSet().getFloat(ctx.index()))); + } + @Override + final Float get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Float.valueOf(ctx.statement().getFloat(ctx.index()))); + } + @Override + final Float get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Float.valueOf(ctx.input().readFloat())); + } + @Override + final int sqltype(Configuration configuration) { + return Types.FLOAT; + } + } + static final class DefaultIntegerBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 1356528214897599147L; + DefaultIntegerBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, Integer value) { + ctx.render().sql(value.toString()); + } - // Most databases don't have such a type. In this case, jOOQ - // emulates the type - default: { - result = (T) Convert.convert(ctx.resultSet().getString(ctx.index()), UUID.class); - break; - } - } + @Override + final void set0(BindingSetStatementContext ctx, Integer value) throws SQLException { + ctx.statement().setInt(ctx.index(), value); } - // The type byte[] is handled earlier. byte[][] can be handled here - else if (type.isArray()) { - switch (ctx.family()) { - case POSTGRES: { - result = pgGetArray(ctx, ctx.resultSet(), type, ctx.index()); - break; - } + @Override + final void set0(BindingSetSQLOutputContext ctx, Integer value) throws SQLException { + ctx.output().writeInt(value); + } - default: - // Note: due to a HSQLDB bug, it is not recommended to call rs.getObject() here: - // See https://sourceforge.net/tracker/?func=detail&aid=3181365&group_id=23316&atid=378131 - result = (T) convertArray(ctx.resultSet().getArray(ctx.index()), (Class) type); - break; - } + @Override + final Integer get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Integer.valueOf(ctx.resultSet().getInt(ctx.index()))); } + @Override + final Integer get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Integer.valueOf(ctx.statement().getInt(ctx.index()))); + } + @Override + final Integer get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Integer.valueOf(ctx.input().readInt())); + } + @Override + final int sqltype(Configuration configuration) { + return Types.INTEGER; + } + } + static final class DefaultLongBinding extends AbstractBinding { - else if (EnumType.class.isAssignableFrom(type)) { - result = (T) getEnumType((Class) type, ctx.resultSet().getString(ctx.index())); + /** + * Generated UID + */ + private static final long serialVersionUID = 7360911614219750448L; + + DefaultLongBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (Record.class.isAssignableFrom(type)) { - switch (ctx.family()) { - case POSTGRES: - result = (T) pgNewRecord(type, null, ctx.resultSet().getObject(ctx.index())); - break; - default: - result = (T) ctx.resultSet().getObject(ctx.index(), typeMap(type, ctx.configuration())); - break; - } + @Override + final void sqlInline0(BindingSQLContext ctx, Long value) { + ctx.render().sql(value.toString()); } - else if (Result.class.isAssignableFrom(type)) { - ResultSet nested = (ResultSet) ctx.resultSet().getObject(ctx.index()); - result = (T) DSL.using(ctx.configuration()).fetch(nested); + + @Override + final void set0(BindingSetStatementContext ctx, Long value) throws SQLException { + + + + + + + ctx.statement().setLong(ctx.index(), value); } - else { - result = (T) unlob(ctx.resultSet().getObject(ctx.index())); + + @Override + final void set0(BindingSetSQLOutputContext ctx, Long value) throws SQLException { + ctx.output().writeLong(value); } - // [#4372] Attach records if possible / required - if (result instanceof Attachable && attachRecords(ctx.configuration())) - ((Attachable) result).attach(ctx.configuration()); + @Override + final Long get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Long.valueOf(ctx.resultSet().getLong(ctx.index()))); + } - ctx.value(converter.from(result)); - } + @Override + final Long get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Long.valueOf(ctx.statement().getLong(ctx.index()))); + } + @Override + final Long get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Long.valueOf(ctx.input().readLong())); + } - private static final LocalDate localDate(Date date) { - return date == null ? null : date.toLocalDate(); + @Override + final int sqltype(Configuration configuration) { + return Types.BIGINT; + } } - private static final LocalTime localTime(Time time) { - return time == null ? null : time.toLocalTime(); - } - private static final LocalDateTime localDateTime(Timestamp timestamp) { - return timestamp == null ? null : timestamp.toLocalDateTime(); - } private static final Pattern LENIENT_OFFSET_PATTERN = Pattern.compile( "(?:(\\d{4}-\\d{2}-\\d{2})[T ])?(\\d{2}:\\d{2}(:\\d{2})?(?:\\.\\d+)?)(?: +)?(([+-])(\\d)?(\\d)(:\\d{2})?)?"); @@ -1722,376 +2348,645 @@ private static final String preparse(String formatted, boolean includeDate) { } } - private static final String replaceZ(String format) { + static final class DefaultOffsetDateTimeBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 2775682497765456476L; + + DefaultOffsetDateTimeBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + + @Override + final void sqlInline0(BindingSQLContext ctx, OffsetDateTime value) { + + // [#5806] H2 doesn't support TIMESTAMP WITH TIME ZONE literals, see + if (ctx.family() == H2) + ctx.render().visit(K_CAST).sql("('").sql(escape(format(value), ctx.render())).sql("' ") + .visit(K_AS).sql(' ').visit(K_TIMESTAMP_WITH_TIME_ZONE).sql(')'); + + - // Replace the ISO standard Z character for UTC, as some databases don't like that - return format.replace("Z", "+00:00"); - } - private static final String format(OffsetTime val) { - return replaceZ(val.format(DateTimeFormatter.ISO_OFFSET_TIME)); - } - private static final String format(OffsetDateTime val) { - // Remove the ISO standard T character, as some databases don't like that - String format = formatISO(val); - return replaceZ(format.substring(0, 10) + ' ' + format.substring(11)); - } - private static final String formatISO(OffsetDateTime val) { - return val.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); - } - @SuppressWarnings("unchecked") - @Override - public void get(BindingGetStatementContext ctx) throws SQLException { - T result = null; - if (type == Blob.class) { - result = (T) ctx.statement().getBlob(ctx.index()); + // Some dialects implement SQL standard time literals + else + ctx.render().visit(K_TIMESTAMP_WITH_TIME_ZONE).sql(" '").sql(escape(format(value), ctx.render())).sql('\''); } - else if (type == Boolean.class) { - result = (T) wasNull(ctx.statement(), Boolean.valueOf(ctx.statement().getBoolean(ctx.index()))); + + @Override + final void set0(BindingSetStatementContext ctx, OffsetDateTime value) throws SQLException { + ctx.statement().setString(ctx.index(), format(value)); } - else if (type == BigInteger.class) { - BigDecimal d = ctx.statement().getBigDecimal(ctx.index()); - result = (T) (d == null ? null : d.toBigInteger()); + + @Override + final void set0(BindingSetSQLOutputContext ctx, OffsetDateTime value) throws SQLException { + // [#6630] TODO support this type + throw new UnsupportedOperationException("Type " + type + " is not supported"); + } + + @Override + final OffsetDateTime get0(BindingGetResultSetContext ctx) throws SQLException { + return offsetDateTime(ctx.resultSet().getString(ctx.index())); + } + + @Override + final OffsetDateTime get0(BindingGetStatementContext ctx) throws SQLException { + return offsetDateTime(ctx.statement().getString(ctx.index())); } - else if (type == BigDecimal.class) { - result = (T) ctx.statement().getBigDecimal(ctx.index()); + + @Override + final OffsetDateTime get0(BindingGetSQLInputContext ctx) throws SQLException { + // [#6630] TODO support this type + throw new UnsupportedOperationException("Type " + type + " is not supported"); } - else if (type == Byte.class) { - result = (T) wasNull(ctx.statement(), Byte.valueOf(ctx.statement().getByte(ctx.index()))); + + @Override + final int sqltype(Configuration configuration) { + + // [#5779] Few JDBC drivers support the JDBC 4.2 TIME[STAMP]_WITH_TIMEZONE types. + return Types.VARCHAR; } - else if (type == byte[].class) { - result = (T) ctx.statement().getBytes(ctx.index()); + + private static final String format(OffsetDateTime val) { + + // Remove the ISO standard T character, as some databases don't like that + String format = formatISO(val); + + // Replace the ISO standard Z character for UTC, as some databases don't like that + return (format.substring(0, 10) + ' ' + format.substring(11)).replace("Z", "+00:00"); } - else if (type == Clob.class) { - result = (T) ctx.statement().getClob(ctx.index()); + + private static final String formatISO(OffsetDateTime val) { + return val.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); } - else if (Tools.isDate(type)) { + } + static final class DefaultOffsetTimeBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 8991629916239335071L; + DefaultOffsetTimeBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, OffsetTime value) { - result = (T) ctx.statement().getDate(ctx.index()); - if (result != null && type == LocalDate.class) - result = (T) ((Date) result).toLocalDate(); + // Some dialects implement SQL standard time literals + ctx.render().visit(K_TIME_WITH_TIME_ZONE).sql(" '").sql(escape(format(value), ctx.render())).sql('\''); } - else if (type == Double.class) { - result = (T) wasNull(ctx.statement(), Double.valueOf(ctx.statement().getDouble(ctx.index()))); - } - else if (type == Float.class) { - result = (T) wasNull(ctx.statement(), Float.valueOf(ctx.statement().getFloat(ctx.index()))); + + @Override + final void set0(BindingSetStatementContext ctx, OffsetTime value) throws SQLException { + String string = format(value); + + + + + + + + ctx.statement().setString(ctx.index(), string); } - else if (type == Integer.class) { - result = (T) wasNull(ctx.statement(), Integer.valueOf(ctx.statement().getInt(ctx.index()))); + + @Override + final void set0(BindingSetSQLOutputContext ctx, OffsetTime value) throws SQLException { + // [#6630] TODO support this type + throw new UnsupportedOperationException("Type " + type + " is not supported"); } - else if (type == Long.class) { - result = (T) wasNull(ctx.statement(), Long.valueOf(ctx.statement().getLong(ctx.index()))); + + @Override + final OffsetTime get0(BindingGetResultSetContext ctx) throws SQLException { + return offsetTime(ctx.resultSet().getString(ctx.index())); } - else if (type == Short.class) { - result = (T) wasNull(ctx.statement(), Short.valueOf(ctx.statement().getShort(ctx.index()))); + + @Override + final OffsetTime get0(BindingGetStatementContext ctx) throws SQLException { + return offsetTime(ctx.statement().getString(ctx.index())); } - else if (type == String.class) { - result = (T) ctx.statement().getString(ctx.index()); + + @Override + final OffsetTime get0(BindingGetSQLInputContext ctx) throws SQLException { + // [#6630] TODO support this type + throw new UnsupportedOperationException("Type " + type + " is not supported"); } - else if (Tools.isTime(type)) { - result = (T) ctx.statement().getTime(ctx.index()); + @Override + final int sqltype(Configuration configuration) { + + // [#5779] Few JDBC drivers support the JDBC 4.2 TIME[STAMP]_WITH_TIMEZONE types. + return Types.VARCHAR; + } - if (result != null && type == LocalTime.class) - result = (T) ((Time) result).toLocalTime(); + private static final String format(OffsetTime val) { + // Replace the ISO standard Z character for UTC, as some databases don't like that + return val.format(DateTimeFormatter.ISO_OFFSET_TIME).replace("Z", "+00:00"); } - else if (Tools.isTimestamp(type)) { - result = (T) ctx.statement().getTimestamp(ctx.index()); + } - if (result != null && type == LocalDateTime.class) - result = (T) ((Timestamp) result).toLocalDateTime(); - } + static final class DefaultOtherBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -650741489151706822L; - else if (type == OffsetTime.class) { - result = (T) offsetTime(ctx.statement().getString(ctx.index())); + DefaultOtherBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == OffsetDateTime.class) { - result = (T) offsetDateTime(ctx.statement().getString(ctx.index())); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + final void set0(BindingSetStatementContext ctx, Object value) throws SQLException { + ((AbstractBinding) binding(value.getClass(), isLob)).set0(ctx, value); } + @Override + final void setNull0(BindingSetStatementContext ctx) throws SQLException { - else if (type == YearToMonth.class) { - if (ctx.family() == POSTGRES) { - Object object = ctx.statement().getObject(ctx.index()); - result = (T) (object == null ? null : PostgresUtils.toYearToMonth(object)); - } - else { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : YearToMonth.valueOf(string)); - } + + + + + + + + + + + + + // [#729] In the absence of the correct JDBC type, try setObject + ctx.statement().setObject(ctx.index(), null); } - else if (type == DayToSecond.class) { - if (ctx.family() == POSTGRES) { - Object object = ctx.statement().getObject(ctx.index()); - result = (T) (object == null ? null : PostgresUtils.toDayToSecond(object)); - } - else { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : DayToSecond.valueOf(string)); - } + + @Override + final void set0(BindingSetSQLOutputContext ctx, Object value) throws SQLException { + throw new UnsupportedOperationException("Type " + type + " is not supported"); } - else if (type == UByte.class) { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : UByte.valueOf(string)); + + @Override + final Object get0(BindingGetResultSetContext ctx) throws SQLException { + return unlob(ctx.resultSet().getObject(ctx.index())); } - else if (type == UShort.class) { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : UShort.valueOf(string)); + + @Override + final Object get0(BindingGetStatementContext ctx) throws SQLException { + return unlob(ctx.statement().getObject(ctx.index())); } - else if (type == UInteger.class) { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : UInteger.valueOf(string)); + + @Override + final Object get0(BindingGetSQLInputContext ctx) throws SQLException { + return unlob(ctx.input().readObject()); } - else if (type == ULong.class) { - String string = ctx.statement().getString(ctx.index()); - result = (T) (string == null ? null : ULong.valueOf(string)); + + @Override + final int sqltype(Configuration configuration) { + return Types.OTHER; } - else if (type == UUID.class) { - switch (ctx.family()) { - // [#1624] Some JDBC drivers natively support the - // java.util.UUID data type - case H2: - case POSTGRES: { - result = (T) ctx.statement().getObject(ctx.index()); - break; + /** + * [#2534] Extract byte[] or String data from a + * LOB, if the argument is a lob. + */ + private static final Object unlob(Object object) throws SQLException { + if (object instanceof Blob) { + Blob blob = (Blob) object; + + try { + return blob.getBytes(1, (int) blob.length()); + } + finally { + JDBCUtils.safeFree(blob); } + } + else if (object instanceof Clob) { + Clob clob = (Clob) object; + try { + return clob.getSubString(1, (int) clob.length()); + } + finally { + JDBCUtils.safeFree(clob); + } + } + return object; + } + } + static final class DefaultRecordBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 2547994476924120818L; + DefaultRecordBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, Record value) { + ctx.render().sql("[UDT]"); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + final void register0(BindingRegisterContext ctx) throws SQLException { - // Most databases don't have such a type. In this case, jOOQ - // emulates the type - default: { - result = (T) Convert.convert(ctx.statement().getString(ctx.index()), UUID.class); - break; - } - } + + + + + + + super.register0(ctx); } - // The type byte[] is handled earlier. byte[][] can be handled here - else if (type.isArray()) { - result = (T) convertArray(ctx.statement().getObject(ctx.index()), (Class)type); + @Override + final void set0(BindingSetStatementContext ctx, Record value) throws SQLException { + ctx.statement().setObject(ctx.index(), value); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + final void setNull0(BindingSetStatementContext ctx) throws SQLException { + + + + + + super.setNull0(ctx); + } - else if (EnumType.class.isAssignableFrom(type)) { - result = (T) getEnumType((Class) type, ctx.statement().getString(ctx.index())); + @Override + final void set0(BindingSetSQLOutputContext ctx, Record value) throws SQLException { + if (value instanceof UDTRecord) + ctx.output().writeObject((UDTRecord) value); + else + throw new UnsupportedOperationException("Type " + type + " is not supported"); } - else if (Record.class.isAssignableFrom(type)) { + + @Override + final Record get0(BindingGetResultSetContext ctx) throws SQLException { switch (ctx.family()) { case POSTGRES: - result = (T) pgNewRecord(type, null, ctx.statement().getObject(ctx.index())); - break; + return pgNewRecord(type, null, ctx.resultSet().getObject(ctx.index())); default: - result = (T) ctx.statement().getObject(ctx.index(), typeMap(type, ctx.configuration())); - break; + return (Record) ctx.resultSet().getObject(ctx.index(), typeMap(type, ctx.configuration())); } } - else if (Result.class.isAssignableFrom(type)) { - ResultSet nested = (ResultSet) ctx.statement().getObject(ctx.index()); - result = (T) DSL.using(ctx.configuration()).fetch(nested); - } - else { - result = (T) ctx.statement().getObject(ctx.index()); - } - - // [#4372] Attach records if possible / required - if (result instanceof Attachable && attachRecords(ctx.configuration())) - ((Attachable) result).attach(ctx.configuration()); - - ctx.value(converter.from(result)); - } - static final Map> typeMap(Class type, Configuration configuration) { - return typeMap(type, configuration, new HashMap>()); - } + @Override + final Record get0(BindingGetStatementContext ctx) throws SQLException { + switch (ctx.family()) { + case POSTGRES: + return pgNewRecord(type, null, ctx.statement().getObject(ctx.index())); - @SuppressWarnings("unchecked") - static final Map> typeMap(Class type, Configuration configuration, Map> result) { - try { - if (UDTRecord.class.isAssignableFrom(type)) { - Class> t = (Class>) type; - result.put(getMappedUDTName(configuration, t), t); - UDTRecord r = t.newInstance(); - for (Field field : r.getUDT().fields()) - typeMap(field.getType(), configuration, result); + default: + return (Record) ctx.statement().getObject(ctx.index(), typeMap(type, ctx.configuration())); } + } + @Override + final Record get0(BindingGetSQLInputContext ctx) throws SQLException { + return (Record) ctx.input().readObject(); + } + @Override + final int sqltype(Configuration configuration) { + return Types.STRUCT; + } + // ------------------------------------------------------------------------- + // XXX: The following section has been added for Postgres UDT support. The + // official Postgres JDBC driver does not implement SQLData and similar + // interfaces. Instead, a string representation of a UDT has to be parsed + // ------------------------------------------------------------------------- + private static final T pgFromString(Class type, String string) { + return pgFromString(Converters.identity(type), string); + } + @SuppressWarnings("unchecked") + private static final T pgFromString(Converter converter, String string) { + Class type = converter.toType(); + if (string == null) + return null; + else if (type == Blob.class) + ; // Not supported + else if (type == Boolean.class) + return (T) Convert.convert(string, Boolean.class); + else if (type == BigInteger.class) + return (T) new BigInteger(string); + else if (type == BigDecimal.class) + return (T) new BigDecimal(string); + else if (type == Byte.class) + return (T) Byte.valueOf(string); + else if (type == byte[].class) + return (T) PostgresUtils.toBytes(string); + else if (type == Clob.class) + ; // Not supported + else if (type == Date.class) + return (T) Date.valueOf(string); + else if (type == Double.class) + return (T) Double.valueOf(string); + else if (type == Float.class) + return (T) Float.valueOf(string); + else if (type == Integer.class) + return (T) Integer.valueOf(string); + else if (type == Long.class) + return (T) Long.valueOf(string); + else if (type == Short.class) + return (T) Short.valueOf(string); + else if (type == String.class) + return (T) string; + else if (type == Time.class) + return (T) Time.valueOf(string); + else if (type == Timestamp.class) + return (T) Timestamp.valueOf(string); + + else if (type == LocalTime.class) + return (T) LocalTime.parse(string); + else if (type == LocalDate.class) + return (T) LocalDate.parse(string); + else if (type == LocalDateTime.class) + return (T) LocalDateTime.parse(string); + else if (type == OffsetTime.class) + return (T) offsetTime(string); + else if (type == OffsetDateTime.class) + return (T) offsetDateTime(string); + + else if (type == UByte.class) + return (T) UByte.valueOf(string); + else if (type == UShort.class) + return (T) UShort.valueOf(string); + else if (type == UInteger.class) + return (T) UInteger.valueOf(string); + else if (type == ULong.class) + return (T) ULong.valueOf(string); + else if (type == UUID.class) + return (T) UUID.fromString(string); + else if (type.isArray()) + return (T) pgNewArray(type, string); + + + + + else if (EnumType.class.isAssignableFrom(type)) + return (T) DefaultEnumTypeBinding.getEnumType((Class) type, string); + else if (Record.class.isAssignableFrom(type)) + return (T) pgNewRecord(type, null, string); + else if (type == Object.class) + return (T) string; + + // [#4964] [#6058] Recurse only if we have a meaningful converter, not the identity converter, + // which would cause a StackOverflowError, here! + else if (type != converter.fromType()) { + Converter c = (Converter) converter; + return c.from(pgFromString(c.fromType(), string)); + } + + throw new UnsupportedOperationException("Class " + type + " is not supported"); + } + + /** + * Create a UDT record from a PGobject + *

+ * Unfortunately, this feature is very poorly documented and true UDT + * support by the PostGreSQL JDBC driver has been postponed for a long time. + * + * @param object An object of type PGobject. The actual argument type cannot + * be expressed in the method signature, as no explicit + * dependency to postgres logic is desired + * @return The converted {@link UDTRecord} + */ + @SuppressWarnings("unchecked") + static final Record pgNewRecord(Class type, Field[] fields, final Object object) { + if (object == null) + return null; + final List values = PostgresUtils.toPGObject(object.toString()); + // [#6404] In the event of an unknown record type, derive the record length from actual values. + // Unfortunately, few column types can be derived from this information. Some possibilities + // for future jOOQ versions include: + // - Integers + // - Numbers + // - Binary data (starts with \x) + // - Temporal data + // - Everything else: VARCHAR + if (fields == null && type == Record.class) + fields = Tools.fields(values.size(), SQLDataType.VARCHAR); - } - catch (Exception e) { - throw new MappingException("Error while collecting type map", e); - } + return Tools.newRecord(true, (Class) type, fields) + .operate(new RecordOperation() { - return result; - } + @Override + public Record operate(Record record) { + Row row = record.fieldsRow(); - @SuppressWarnings("unchecked") - @Override - public void get(BindingGetSQLInputContext ctx) throws SQLException { - T result = null; + for (int i = 0; i < row.size(); i++) + pgSetValue(record, row.field(i), values.get(i)); - if (type == Blob.class) { - result = (T) ctx.input().readBlob(); - } - else if (type == Boolean.class) { - result = (T) wasNull(ctx.input(), Boolean.valueOf(ctx.input().readBoolean())); - } - else if (type == BigInteger.class) { - BigDecimal d = ctx.input().readBigDecimal(); - result = (T) (d == null ? null : d.toBigInteger()); - } - else if (type == BigDecimal.class) { - result = (T) ctx.input().readBigDecimal(); + return record; + } + }); } - else if (type == Byte.class) { - result = (T) wasNull(ctx.input(), Byte.valueOf(ctx.input().readByte())); + + private static final void pgSetValue(Record record, Field field, String value) { + record.set(field, pgFromString(field.getConverter(), value)); } - else if (type == byte[].class) { - // [#1327] Oracle cannot deserialise BLOBs as byte[] from SQLInput - if (isLob) { - Blob blob = null; - try { - blob = ctx.input().readBlob(); - result = (T) (blob == null ? null : blob.getBytes(1, (int) blob.length())); + /** + * Create an array from a String + *

+ * Unfortunately, this feature is very poorly documented and true UDT + * support by the PostGreSQL JDBC driver has been postponed for a long time. + * + * @param string A String representation of an array + * @return The converted array + */ + private static final Object[] pgNewArray(Class type, String string) { + if (string == null) { + return null; + } + + try { + Class component = type.getComponentType(); + List values = PostgresUtils.toPGArray(string); + + if (values.isEmpty()) { + return (Object[]) java.lang.reflect.Array.newInstance(component, 0); } - finally { - safeFree(blob); + else { + Object[] result = (Object[]) java.lang.reflect.Array.newInstance(component, values.size()); + + for (int i = 0; i < values.size(); i++) + result[i] = pgFromString(type.getComponentType(), values.get(i)); + + return result; } } - else { - result = (T) ctx.input().readBytes(); + catch (Exception e) { + throw new DataTypeException("Error while creating array", e); } } - else if (type == Clob.class) { - result = (T) ctx.input().readClob(); + } + + static final class DefaultResultBinding extends AbstractBinding, U> { + + /** + * Generated UID + */ + private static final long serialVersionUID = -2148875780733374224L; + + DefaultResultBinding(Converter, U> converter, boolean isLob) { + super(converter, isLob); } - else if (type == Date.class) { + @Override + final void set0(BindingSetStatementContext ctx, Result value) throws SQLException { + throw new UnsupportedOperationException("Cannot bind a value of type Result to a PreparedStatement"); + } + @Override + final void set0(BindingSetSQLOutputContext ctx, Result value) throws SQLException { + throw new UnsupportedOperationException("Cannot bind a value of type Result to a SQLOutput"); + } + @Override + final Result get0(BindingGetResultSetContext ctx) throws SQLException { + ResultSet nested = (ResultSet) ctx.resultSet().getObject(ctx.index()); + return DSL.using(ctx.configuration()).fetch(nested); + } + @Override + final Result get0(BindingGetStatementContext ctx) throws SQLException { + ResultSet nested = (ResultSet) ctx.statement().getObject(ctx.index()); + return DSL.using(ctx.configuration()).fetch(nested); + } + @Override + final Result get0(BindingGetSQLInputContext ctx) throws SQLException { + throw new UnsupportedOperationException("Cannot get a value of type Result from a SQLInput"); + } + @Override + final int sqltype(Configuration configuration) { + switch (configuration.family()) { - result = (T) ctx.input().readDate(); - } - else if (type == Double.class) { - result = (T) wasNull(ctx.input(), Double.valueOf(ctx.input().readDouble())); - } - else if (type == Float.class) { - result = (T) wasNull(ctx.input(), Float.valueOf(ctx.input().readFloat())); - } - else if (type == Integer.class) { - result = (T) wasNull(ctx.input(), Integer.valueOf(ctx.input().readInt())); - } - else if (type == Long.class) { - result = (T) wasNull(ctx.input(), Long.valueOf(ctx.input().readLong())); - } - else if (type == Short.class) { - result = (T) wasNull(ctx.input(), Short.valueOf(ctx.input().readShort())); - } - else if (type == String.class) { - result = (T) ctx.input().readString(); + case H2: + return -10; // OracleTypes.CURSOR; + + case POSTGRES: + default: + return Types.OTHER; + } } - else if (type == Time.class) { - result = (T) ctx.input().readTime(); + } + + static final class DefaultShortBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 8935720621737085226L; + + DefaultShortBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == Timestamp.class) { - result = (T) ctx.input().readTimestamp(); + + @Override + final void sqlInline0(BindingSQLContext ctx, Short value) { + ctx.render().sql(value.toString()); } - else if (type == YearToMonth.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : YearToMonth.valueOf(string)); + + @Override + final void set0(BindingSetStatementContext ctx, Short value) throws SQLException { + ctx.statement().setShort(ctx.index(), value); } - else if (type == DayToSecond.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : DayToSecond.valueOf(string)); + + @Override + final void set0(BindingSetSQLOutputContext ctx, Short value) throws SQLException { + ctx.output().writeShort(value); } - else if (type == UByte.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : UByte.valueOf(string)); + + @Override + final Short get0(BindingGetResultSetContext ctx) throws SQLException { + return wasNull(ctx.resultSet(), Short.valueOf(ctx.resultSet().getShort(ctx.index()))); } - else if (type == UShort.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : UShort.valueOf(string)); + + @Override + final Short get0(BindingGetStatementContext ctx) throws SQLException { + return wasNull(ctx.statement(), Short.valueOf(ctx.statement().getShort(ctx.index()))); } - else if (type == UInteger.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : UInteger.valueOf(string)); + + @Override + final Short get0(BindingGetSQLInputContext ctx) throws SQLException { + return wasNull(ctx.input(), Short.valueOf(ctx.input().readShort())); } - else if (type == ULong.class) { - String string = ctx.input().readString(); - result = (T) (string == null ? null : ULong.valueOf(string)); + + @Override + final int sqltype(Configuration configuration) { + return Types.SMALLINT; } - else if (type == UUID.class) { - result = (T) Convert.convert(ctx.input().readString(), UUID.class); + } + + static final class DefaultStringBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 4232459541239942932L; + + DefaultStringBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - // The type byte[] is handled earlier. byte[][] can be handled here - else if (type.isArray()) { - Array array = ctx.input().readArray(); - result = (T) (array == null ? null : array.getArray()); + @Override + final void set0(BindingSetStatementContext ctx, String value) throws SQLException { + ctx.statement().setString(ctx.index(), value); } + @Override + final void set0(BindingSetSQLOutputContext ctx, String value) throws SQLException { + - else if (EnumType.class.isAssignableFrom(type)) { - result = (T) getEnumType((Class) type, ctx.input().readString()); - } - else if (UDTRecord.class.isAssignableFrom(type)) { - result = (T) ctx.input().readObject(); - } - else { - result = (T) unlob(ctx.input().readObject()); - } - ctx.value(converter.from(result)); - } @@ -2110,15 +3005,48 @@ else if (UDTRecord.class.isAssignableFrom(type)) { + ctx.output().writeString(value); + } + @Override + final String get0(BindingGetResultSetContext ctx) throws SQLException { + return ctx.resultSet().getString(ctx.index()); + } + @Override + final String get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getString(ctx.index()); + } + @Override + final String get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readString(); + } + @Override + final int sqltype(Configuration configuration) { + return Types.VARCHAR; + } + } + static final class DefaultTimeBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = -2563220967846617288L; + DefaultTimeBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + @Override + final void sqlInline0(BindingSQLContext ctx, Time value) { + // The SQLite JDBC driver does not implement the escape syntax + // [#1253] Sybase does not implement time literals + if (asList(SQLITE).contains(ctx.family())) + ctx.render().sql('\'').sql(new SimpleDateFormat("HH:mm:ss").format(value)).sql('\''); @@ -2127,7 +3055,13 @@ else if (UDTRecord.class.isAssignableFrom(type)) { + // [#1253] Derby doesn't support the standard literal + else if (ctx.family() == DERBY) + ctx.render().visit(K_TIME).sql("('").sql(escape(value, ctx.render())).sql("')"); + // [#3648] Circumvent a MySQL bug related to date literals + else if (ctx.family() == MYSQL) + ctx.render().sql("{t '").sql(escape(value, ctx.render())).sql("'}"); @@ -2139,77 +3073,74 @@ else if (UDTRecord.class.isAssignableFrom(type)) { - /** - * [#2534] Extract byte[] or String data from a - * LOB, if the argument is a lob. - */ - private static final Object unlob(Object object) throws SQLException { - if (object instanceof Blob) { - Blob blob = (Blob) object; - try { - return blob.getBytes(1, (int) blob.length()); - } - finally { - JDBCUtils.safeFree(blob); - } + // Most dialects implement SQL standard time literals + else + ctx.render().visit(K_TIME).sql(" '").sql(escape(value, ctx.render())).sql('\''); } - else if (object instanceof Clob) { - Clob clob = (Clob) object; - try { - return clob.getSubString(1, (int) clob.length()); - } - finally { - JDBCUtils.safeFree(clob); - } + @Override + final void set0(BindingSetStatementContext ctx, Time value) throws SQLException { + if (ctx.family() == SQLITE) + ctx.statement().setString(ctx.index(), value.toString()); + else + ctx.statement().setTime(ctx.index(), value); } - return object; - } + @Override + final void set0(BindingSetSQLOutputContext ctx, Time value) throws SQLException { + ctx.output().writeTime(value); + } - @SuppressWarnings("unchecked") - private static final E getEnumType(Class type, String literal) { - try { - EnumType[] list = Tools.enums(type); + @Override + final Time get0(BindingGetResultSetContext ctx) throws SQLException { - for (EnumType e : list) - if (e.getLiteral().equals(literal)) - return (E) e; - } - catch (Exception e) { - throw new DataTypeException("Unknown enum literal found : " + literal); - } + // SQLite's type affinity needs special care... + if (ctx.family() == SQLDialect.SQLITE) { + String time = ctx.resultSet().getString(ctx.index()); + return time == null ? null : new Time(parse(Time.class, time)); + } - return null; - } + else { + return ctx.resultSet().getTime(ctx.index()); + } + } - private static final Object[] convertArray(Object array, Class type) throws SQLException { - if (array instanceof Object[]) { - return Convert.convert(array, type); + @Override + final Time get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getTime(ctx.index()); } - else if (array instanceof Array) { - return convertArray((Array) array, type); + + @Override + final Time get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readTime(); } - return null; + @Override + final int sqltype(Configuration configuration) { + return Types.TIME; + } } - private static final Object[] convertArray(Array array, Class type) throws SQLException { - if (array != null) { - return Convert.convert(array.getArray(), type); + static final class DefaultTimestampBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 289387167549159015L; + + DefaultTimestampBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - return null; - } + @Override + final void sqlInline0(BindingSQLContext ctx, Timestamp value) { - private static final Date getDate(SQLDialect family, ResultSet rs, int index) throws SQLException { + // The SQLite JDBC driver does not implement the escape syntax + // [#1253] Sybase does not implement timestamp literals + if (asList(SQLITE).contains(ctx.family())) + ctx.render().sql('\'').sql(escape(value, ctx.render())).sql('\''); - // SQLite's type affinity needs special care... - if (family == SQLDialect.SQLITE) { - String date = rs.getString(index); - return date == null ? null : new Date(parse(Date.class, date)); - } @@ -2219,373 +3150,438 @@ private static final Date getDate(SQLDialect family, ResultSet rs, int index) th - else { - return rs.getDate(index); - } - } - private static final Time getTime(SQLDialect family, ResultSet rs, int index) throws SQLException { - // SQLite's type affinity needs special care... - if (family == SQLDialect.SQLITE) { - String time = rs.getString(index); - return time == null ? null : new Time(parse(Time.class, time)); - } - else { - return rs.getTime(index); - } - } + // [#1253] Derby doesn't support the standard literal + else if (ctx.family() == DERBY) + ctx.render().visit(K_TIMESTAMP).sql("('").sql(escape(value, ctx.render())).sql("')"); - private static final Timestamp getTimestamp(SQLDialect family, ResultSet rs, int index) throws SQLException { + // CUBRID timestamps have no fractional seconds + else if (ctx.family() == CUBRID) + ctx.render().visit(K_DATETIME).sql(" '").sql(escape(value, ctx.render())).sql('\''); - // SQLite's type affinity needs special care... - if (family == SQLDialect.SQLITE) { - String timestamp = rs.getString(index); - return timestamp == null ? null : new Timestamp(parse(Timestamp.class, timestamp)); - } + // [#3648] Circumvent a MySQL bug related to date literals + else if (ctx.family() == MYSQL) + ctx.render().sql("{ts '").sql(escape(value, ctx.render())).sql("'}"); - else { - return rs.getTimestamp(index); + // Most dialects implement SQL standard timestamp literals + else + ctx.render().visit(K_TIMESTAMP).sql(" '").sql(escape(value, ctx.render())).sql('\''); } - } - - private static final long parse(Class type, String date) throws SQLException { - // Try reading a plain number first - try { - return Long.valueOf(date); + @Override + final void set0(BindingSetStatementContext ctx, Timestamp value) throws SQLException { + if (ctx.family() == SQLITE) + ctx.statement().setString(ctx.index(), value.toString()); + else + ctx.statement().setTimestamp(ctx.index(), value); } - // If that fails, try reading a formatted date - catch (NumberFormatException e) { + @Override + final void set0(BindingSetSQLOutputContext ctx, Timestamp value) throws SQLException { + ctx.output().writeTimestamp(value); + } - if (type == Timestamp.class) - return Timestamp.valueOf(date).getTime(); + @Override + final Timestamp get0(BindingGetResultSetContext ctx) throws SQLException { - // Dates may come with " 00:00:00". This is safely trimming time information - if (type == Date.class) - return Date.valueOf(date.split(" ")[0]).getTime(); + // SQLite's type affinity needs special care... + if (ctx.family() == SQLDialect.SQLITE) { + String timestamp = ctx.resultSet().getString(ctx.index()); + return timestamp == null ? null : new Timestamp(parse(Timestamp.class, timestamp)); + } - if (type == Time.class) - return Time.valueOf(date).getTime(); + else { + return ctx.resultSet().getTimestamp(ctx.index()); + } + } - throw new SQLException("Could not parse date " + date, e); + @Override + final Timestamp get0(BindingGetStatementContext ctx) throws SQLException { + return ctx.statement().getTimestamp(ctx.index()); } - } - // ------------------------------------------------------------------------- - // XXX: The following section has been added for Postgres UDT support. The - // official Postgres JDBC driver does not implement SQLData and similar - // interfaces. Instead, a string representation of a UDT has to be parsed - // ------------------------------------------------------------------------- + @Override + final Timestamp get0(BindingGetSQLInputContext ctx) throws SQLException { + return ctx.input().readTimestamp(); + } - private static final T pgFromString(Class type, String string) { - return pgFromString(Converters.identity(type), string); + @Override + final int sqltype(Configuration configuration) { + return Types.TIMESTAMP; + } } - @SuppressWarnings("unchecked") - private static final T pgFromString(Converter converter, String string) { - Class type = converter.toType(); + static final class DefaultUByteBinding extends AbstractBinding { - if (string == null) { - return null; - } - else if (type == Blob.class) { - // Not supported - } - else if (type == Boolean.class) { - return (T) Convert.convert(string, Boolean.class); - } - else if (type == BigInteger.class) { - return (T) new BigInteger(string); - } - else if (type == BigDecimal.class) { - return (T) new BigDecimal(string); + /** + * Generated UID + */ + private static final long serialVersionUID = -101167998250685198L; + + DefaultUByteBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == Byte.class) { - return (T) Byte.valueOf(string); + + @Override + final void sqlInline0(BindingSQLContext ctx, UByte value) { + ctx.render().sql(value.toString()); } - else if (type == byte[].class) { - return (T) PostgresUtils.toBytes(string); + + @Override + final void set0(BindingSetStatementContext ctx, UByte value) throws SQLException { + ctx.statement().setShort(ctx.index(), value.shortValue()); } - else if (type == Clob.class) { - // Not supported + + @Override + final void set0(BindingSetSQLOutputContext ctx, UByte value) throws SQLException { + ctx.output().writeShort(value.shortValue()); } - else if (type == Date.class) { - return (T) Date.valueOf(string); + + @Override + final UByte get0(BindingGetResultSetContext ctx) throws SQLException { + return Convert.convert(ctx.resultSet().getString(ctx.index()), UByte.class); } - else if (type == Double.class) { - return (T) Double.valueOf(string); + + @Override + final UByte get0(BindingGetStatementContext ctx) throws SQLException { + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : UByte.valueOf(string); } - else if (type == Float.class) { - return (T) Float.valueOf(string); + + @Override + final UByte get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : UByte.valueOf(string); } - else if (type == Integer.class) { - return (T) Integer.valueOf(string); + + @Override + final int sqltype(Configuration configuration) { + return Types.SMALLINT; } - else if (type == Long.class) { - return (T) Long.valueOf(string); + } + + static final class DefaultUIntegerBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 1437279656720185207L; + + DefaultUIntegerBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == Short.class) { - return (T) Short.valueOf(string); + + @Override + final void sqlInline0(BindingSQLContext ctx, UInteger value) { + ctx.render().sql(value.toString()); } - else if (type == String.class) { - return (T) string; + + @Override + final void set0(BindingSetStatementContext ctx, UInteger value) throws SQLException { + + + + + + ctx.statement().setLong(ctx.index(), value.longValue()); } - else if (type == Time.class) { - return (T) Time.valueOf(string); + + @Override + final void set0(BindingSetSQLOutputContext ctx, UInteger value) throws SQLException { + ctx.output().writeLong(value.longValue()); } - else if (type == Timestamp.class) { - return (T) Timestamp.valueOf(string); + + @Override + final UInteger get0(BindingGetResultSetContext ctx) throws SQLException { + return Convert.convert(ctx.resultSet().getString(ctx.index()), UInteger.class); } - else if (type == LocalTime.class) { - return (T) LocalTime.parse(string); + @Override + final UInteger get0(BindingGetStatementContext ctx) throws SQLException { + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : UInteger.valueOf(string); } - else if (type == LocalDate.class) { - return (T) LocalDate.parse(string); + + @Override + final UInteger get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : UInteger.valueOf(string); } - else if (type == LocalDateTime.class) { - return (T) LocalDateTime.parse(string); + + @Override + final int sqltype(Configuration configuration) { + return Types.BIGINT; } - else if (type == OffsetTime.class) { - return (T) offsetTime(string); + } + + static final class DefaultULongBinding extends AbstractBinding { + + /** + * Generated UID + */ + private static final long serialVersionUID = 4891128447530113299L; + + DefaultULongBinding(Converter converter, boolean isLob) { + super(converter, isLob); } - else if (type == OffsetDateTime.class) { - return (T) offsetDateTime(string); + + @Override + final void sqlInline0(BindingSQLContext ctx, ULong value) { + ctx.render().sql(value.toString()); } - else if (type == UByte.class) { - return (T) UByte.valueOf(string); + @Override + final void set0(BindingSetStatementContext ctx, ULong value) throws SQLException { + + + + + + ctx.statement().setBigDecimal(ctx.index(), new BigDecimal(value.toString())); } - else if (type == UShort.class) { - return (T) UShort.valueOf(string); + + @Override + final void set0(BindingSetSQLOutputContext ctx, ULong value) throws SQLException { + ctx.output().writeBigDecimal(new BigDecimal(value.toString())); } - else if (type == UInteger.class) { - return (T) UInteger.valueOf(string); + + @Override + final ULong get0(BindingGetResultSetContext ctx) throws SQLException { + return Convert.convert(ctx.resultSet().getString(ctx.index()), ULong.class); } - else if (type == ULong.class) { - return (T) ULong.valueOf(string); + + @Override + final ULong get0(BindingGetStatementContext ctx) throws SQLException { + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : ULong.valueOf(string); } - else if (type == UUID.class) { - return (T) UUID.fromString(string); + + @Override + final ULong get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : ULong.valueOf(string); } - else if (type.isArray()) { - return (T) pgNewArray(type, string); + + @Override + final int sqltype(Configuration configuration) { + return Types.DECIMAL; } + } + static final class DefaultUShortBinding extends AbstractBinding { + /** + * Generated UID + */ + private static final long serialVersionUID = 2539811197808516971L; + DefaultUShortBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } + + @Override + final void sqlInline0(BindingSQLContext ctx, UShort value) { + ctx.render().sql(value.toString()); + } + @Override + final void set0(BindingSetStatementContext ctx, UShort value) throws SQLException { + ctx.statement().setInt(ctx.index(), value.intValue()); + } - else if (EnumType.class.isAssignableFrom(type)) { - return (T) getEnumType((Class) type, string); + @Override + final void set0(BindingSetSQLOutputContext ctx, UShort value) throws SQLException { + ctx.output().writeInt(value.intValue()); } - else if (Record.class.isAssignableFrom(type)) { - return (T) pgNewRecord(type, null, string); + + @Override + final UShort get0(BindingGetResultSetContext ctx) throws SQLException { + return Convert.convert(ctx.resultSet().getString(ctx.index()), UShort.class); } - else if (type == Object.class) { - return (T) string; + + @Override + final UShort get0(BindingGetStatementContext ctx) throws SQLException { + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : UShort.valueOf(string); } - // [#4964] [#6058] Recurse only if we have a meaningful converter, not the identity converter, - // which would cause a StackOverflowError, here! - else if (type != converter.fromType()) { - Converter c = (Converter) converter; - return c.from(pgFromString(c.fromType(), string)); + @Override + final UShort get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : UShort.valueOf(string); } - throw new UnsupportedOperationException("Class " + type + " is not supported"); + @Override + final int sqltype(Configuration configuration) { + return Types.INTEGER; + } } - /** - * Create a UDT record from a PGobject - *

- * Unfortunately, this feature is very poorly documented and true UDT - * support by the PostGreSQL JDBC driver has been postponed for a long time. - * - * @param object An object of type PGobject. The actual argument type cannot - * be expressed in the method signature, as no explicit - * dependency to postgres logic is desired - * @return The converted {@link UDTRecord} - */ - @SuppressWarnings("unchecked") - static final Record pgNewRecord(Class type, Field[] fields, final Object object) { - if (object == null) - return null; + static final class DefaultUUIDBinding extends AbstractBinding { - final List values = PostgresUtils.toPGObject(object.toString()); + /** + * Generated UUID + */ + private static final long serialVersionUID = -6616291625634347383L; - // [#6404] In the event of an unknown record type, derive the record length from actual values. - // Unfortunately, few column types can be derived from this information. Some possibilities - // for future jOOQ versions include: - // - Integers - // - Numbers - // - Binary data (starts with \x) - // - Temporal data - // - Everything else: VARCHAR - if (fields == null && type == Record.class) - fields = Tools.fields(values.size(), SQLDataType.VARCHAR); + DefaultUUIDBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } - return Tools.newRecord(true, (Class) type, fields) - .operate(new RecordOperation() { + @Override + final void set0(BindingSetStatementContext ctx, UUID value) throws SQLException { + switch (ctx.family()) { - @Override - public Record operate(Record record) { - Row row = record.fieldsRow(); + // [#1624] Some JDBC drivers natively support the + // java.util.UUID data type + case H2: + case POSTGRES: { + ctx.statement().setObject(ctx.index(), value); + break; + } - for (int i = 0; i < row.size(); i++) - pgSetValue(record, row.field(i), values.get(i)); - return record; - } - }); - } - /** - * Workarounds for the unimplemented Postgres JDBC driver features - */ - @SuppressWarnings("unchecked") - private static final T pgGetArray(Scope ctx, ResultSet rs, Class type, int index) throws SQLException { - // Get the JDBC Array and check for null. If null, that's OK - Array array = null; - try { - array = rs.getArray(index); - if (array == null) { - return null; - } - // Try fetching a Java Object[]. That's gonna work for non-UDT types - try { - // [#5633] Special treatment for this type. - // [#5586] [#5613] TODO: Improve PostgreSQL array deserialisation. - if (byte[][].class == type) - throw new ControlFlowSignal("GOTO the next array deserialisation strategy"); - else - return (T) convertArray(array, (Class) type); + + // Most databases don't have such a type. In this case, jOOQ + // emulates the type + default: { + ctx.statement().setString(ctx.index(), value.toString()); + break; + } } + } - // This might be a UDT (not implemented exception...) - catch (Exception e) { - List result = new ArrayList(); - ResultSet arrayRs = null; + @Override + final void set0(BindingSetSQLOutputContext ctx, UUID value) throws SQLException { + ctx.output().writeString(value.toString()); + } + + @Override + final UUID get0(BindingGetResultSetContext ctx) throws SQLException { + switch (ctx.family()) { + + // [#1624] Some JDBC drivers natively support the + // java.util.UUID data type + case H2: + case POSTGRES: + return (UUID) ctx.resultSet().getObject(ctx.index()); - // Try fetching the array as a JDBC ResultSet - try { - arrayRs = array.getResultSet(); - while (arrayRs.next()) { - DefaultBindingGetResultSetContext out = new DefaultBindingGetResultSetContext(ctx.configuration(), ctx.data(), arrayRs, 2); - new DefaultBinding(Converters.identity((Class) type.getComponentType()), false).get(out); - result.add(out.value()); - } - } - // That might fail too, then we don't know any further... - catch (Exception fatal) { - String string = null; - try { - string = rs.getString(index); - } - catch (SQLException ignore) {} - log.error("Cannot parse array", string, fatal); - return null; - } - finally { - safeClose(arrayRs); - } - return (T) convertArray(result.toArray(), (Class) type); + + + // Most databases don't have such a type. In this case, jOOQ + // emulates the type + default: + return Convert.convert(ctx.resultSet().getString(ctx.index()), UUID.class); } } - finally { - safeFree(array); - } - } + @Override + final UUID get0(BindingGetStatementContext ctx) throws SQLException { + switch (ctx.family()) { + + // [#1624] Some JDBC drivers natively support the + // java.util.UUID data type + case H2: + case POSTGRES: + return (UUID) ctx.statement().getObject(ctx.index()); - /** - * Create an array from a String - *

- * Unfortunately, this feature is very poorly documented and true UDT - * support by the PostGreSQL JDBC driver has been postponed for a long time. - * - * @param string A String representation of an array - * @return The converted array - */ - private static final Object[] pgNewArray(Class type, String string) { - if (string == null) { - return null; - } - try { - Class component = type.getComponentType(); - List values = PostgresUtils.toPGArray(string); - if (values.isEmpty()) { - return (Object[]) java.lang.reflect.Array.newInstance(component, 0); - } - else { - Object[] result = (Object[]) java.lang.reflect.Array.newInstance(component, values.size()); - for (int i = 0; i < values.size(); i++) - result[i] = pgFromString(type.getComponentType(), values.get(i)); - return result; + + + + // Most databases don't have such a type. In this case, jOOQ + // emulates the type + default: + return Convert.convert(ctx.statement().getString(ctx.index()), UUID.class); } } - catch (Exception e) { - throw new DataTypeException("Error while creating array", e); + + @Override + final UUID get0(BindingGetSQLInputContext ctx) throws SQLException { + return Convert.convert(ctx.input().readString(), UUID.class); } - } - static final void pgSetValue(Record record, Field field, String value) { - record.set(field, pgFromString(field.getConverter(), value)); + @Override + final int sqltype(Configuration configuration) { + return Types.VARCHAR; + } } - private static final void pgRenderEnumCast(RenderContext render, Class type) { + static final class DefaultYearToMonthBinding extends AbstractBinding { - @SuppressWarnings("unchecked") - Class enumType = (Class) ( - type.isArray() ? type.getComponentType() : type); + /** + * Generated UID + */ + private static final long serialVersionUID = 6417965474063152673L; - // [#968] Don't cast "synthetic" enum types (note, val can be null!) - // [#4427] Java Enum agnostic implementation will work for Scala also - EnumType[] enums = Tools.enums(enumType); + DefaultYearToMonthBinding(Converter converter, boolean isLob) { + super(converter, isLob); + } - if (enums == null || enums.length == 0) - throw new IllegalArgumentException("Not a valid EnumType : " + type); + @Override + final void set0(BindingSetStatementContext ctx, YearToMonth value) throws SQLException { - Schema schema = enums[0].getSchema(); - if (schema != null) { - render.sql("::"); + // [#566] Interval data types are best bound as Strings + if (ctx.family() == POSTGRES) + ctx.statement().setObject(ctx.index(), toPGInterval(value)); + else + ctx.statement().setString(ctx.index(), value.toString()); + } - schema = using(render.configuration()).map(schema); - if (schema != null && TRUE.equals(render.configuration().settings().isRenderSchema())) { - render.visit(schema); - render.sql('.'); - } + @Override + final void set0(BindingSetSQLOutputContext ctx, YearToMonth value) throws SQLException { + ctx.output().writeString(value.toString()); + } - render.visit(name(enums[0].getName())); + @Override + final YearToMonth get0(BindingGetResultSetContext ctx) throws SQLException { + if (ctx.family() == POSTGRES) { + Object object = ctx.resultSet().getObject(ctx.index()); + return object == null ? null : PostgresUtils.toYearToMonth(object); + } + else { + String string = ctx.resultSet().getString(ctx.index()); + return string == null ? null : YearToMonth.valueOf(string); + } } - if (type.isArray()) - render.sql("[]"); - } + @Override + final YearToMonth get0(BindingGetStatementContext ctx) throws SQLException { + if (ctx.family() == POSTGRES) { + Object object = ctx.statement().getObject(ctx.index()); + return object == null ? null : PostgresUtils.toYearToMonth(object); + } + else { + String string = ctx.statement().getString(ctx.index()); + return string == null ? null : YearToMonth.valueOf(string); + } + } - // ----------------------------------------------------------------------------------------------------------------- - // Object API - // ----------------------------------------------------------------------------------------------------------------- + @Override + final YearToMonth get0(BindingGetSQLInputContext ctx) throws SQLException { + String string = ctx.input().readString(); + return string == null ? null : YearToMonth.valueOf(string); + } - @Override - public String toString() { - return "DefaultBinding [type=" + type + ", converter=" + converter + "]"; + @Override + final int sqltype(Configuration configuration) { + return Types.VARCHAR; + } } } diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java index b885e61269..70bc4b8c32 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java @@ -36,6 +36,7 @@ import static java.util.Collections.unmodifiableCollection; import static org.jooq.Nullability.NOT_NULL; +import static org.jooq.impl.DefaultBinding.binding; import static org.jooq.impl.SQLDataType.BLOB; import static org.jooq.impl.SQLDataType.CLOB; import static org.jooq.impl.SQLDataType.NCLOB; @@ -59,7 +60,6 @@ import org.jooq.Binding; import org.jooq.Configuration; import org.jooq.Converter; -import org.jooq.Converters; import org.jooq.DataType; import org.jooq.EnumType; import org.jooq.Field; @@ -317,9 +317,7 @@ public DefaultDataType(SQLDialect dialect, Class type, String typeName, Strin } } - this.binding = binding != null - ? binding - : new DefaultBinding(Converters.identity(type), this.isLob()); + this.binding = binding != null ? binding : binding(type, isLob()); this.tType = this.binding.converter().fromType(); } @@ -717,7 +715,7 @@ public final DataType asConvertedDataType(Binding newBindin return (DataType) this; if (newBinding == null) - newBinding = (Binding) new DefaultBinding(Converters.identity(getType()), isLob()); + newBinding = (Binding) DefaultBinding.binding(getType(), isLob()); return new ConvertedDataType(this, newBinding); } diff --git a/jOOQ/src/main/java/org/jooq/impl/RowField.java b/jOOQ/src/main/java/org/jooq/impl/RowField.java index 4faf0c7e26..719d1c962c 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RowField.java +++ b/jOOQ/src/main/java/org/jooq/impl/RowField.java @@ -34,6 +34,8 @@ */ package org.jooq.impl; +import static org.jooq.impl.DefaultBinding.binding; +import static org.jooq.impl.DefaultBinding.DefaultRecordBinding.pgNewRecord; import static org.jooq.impl.Tools.DataKey.DATA_LIST_ALREADY_INDENTED; import org.jooq.Context; @@ -63,11 +65,11 @@ final class RowField extends AbstractField< @SuppressWarnings({ "serial", "unchecked", "rawtypes" }) RowField(final ROW row, Name as) { - super(as, (DataType) SQLDataType.RECORD, "", new DefaultBinding(new Converter() { + super(as, (DataType) SQLDataType.RECORD, "", binding(new Converter() { @Override public REC from(final Object t) { // So far, this is only supported for PostgreSQL - return (REC) (t == null ? null : DefaultBinding.pgNewRecord(Record.class, row.fields(), t)); + return (REC) (t == null ? null : pgNewRecord(Record.class, row.fields(), t)); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index f1350d2749..3c0f12286f 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -556,6 +556,12 @@ enum DataKey { "&" }; + /** + * All hexadecimal digits accessible through array index, e.g. + * HEX_DIGITS[15] == 'f'. + */ + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + // ------------------------------------------------------------------------ // XXX: Record constructors and related methods // ------------------------------------------------------------------------ @@ -4115,4 +4121,32 @@ else if (field instanceof String) else throw new IllegalArgumentException("Field type not supported: " + field); } + + /** + * Convert a byte array to a hex encoded string. + * + * @param value the byte array + * @param len the number of bytes to encode + * @return the hex encoded string + */ + static final String convertBytesToHex(byte[] value, int len) { + char[] buff = new char[len + len]; + char[] hex = HEX_DIGITS; + for (int i = 0; i < len; i++) { + int c = value[i] & 0xff; + buff[i + i] = hex[c >> 4]; + buff[i + i + 1] = hex[c & 0xf]; + } + return new String(buff); + } + + /** + * Convert a byte array to a hex encoded string. + * + * @param value the byte array + * @return the hex encoded string + */ + static final String convertBytesToHex(byte[] value) { + return convertBytesToHex(value, value.length); + } }