/** * Copyright (c) 2009-2012, Lukas Eder, lukas.eder@gmail.com * All rights reserved. * * This software is licensed to you under the Apache License, Version 2.0 * (the "License"); You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * . Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * . Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * . Neither the name "jOOQ" nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.jooq.impl; import static java.lang.Integer.toOctalString; import static java.util.Arrays.asList; import static org.jooq.SQLDialect.ASE; import static org.jooq.SQLDialect.CUBRID; import static org.jooq.SQLDialect.DB2; import static org.jooq.SQLDialect.DERBY; import static org.jooq.SQLDialect.FIREBIRD; import static org.jooq.SQLDialect.H2; import static org.jooq.SQLDialect.HSQLDB; import static org.jooq.SQLDialect.INGRES; import static org.jooq.SQLDialect.MYSQL; import static org.jooq.SQLDialect.ORACLE; import static org.jooq.SQLDialect.POSTGRES; import static org.jooq.SQLDialect.SQLITE; import static org.jooq.SQLDialect.SQLSERVER; import static org.jooq.SQLDialect.SYBASE; import static org.jooq.tools.StringUtils.leftPad; import java.math.BigDecimal; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; import java.util.Arrays; import org.jooq.ArrayRecord; import org.jooq.BindContext; import org.jooq.Converter; import org.jooq.DataType; import org.jooq.EnumType; import org.jooq.Param; import org.jooq.RenderContext; import org.jooq.SQLDialect; import org.jooq.UDTRecord; import org.jooq.tools.StringUtils; import org.jooq.types.Interval; /** * @author Lukas Eder */ class Val extends AbstractField implements Param { private static final long serialVersionUID = 6807729087019209084L; private static final char[] HEX = "0123456789abcdef".toCharArray(); private final String paramName; private T value; private boolean inline; Val(T value, DataType type) { this(value, type, null); } Val(T value, DataType type, String paramName) { super(name(value, paramName), type); this.paramName = paramName; this.value = value; } /** * A utility method that generates a field name. *

*

    *
  • If paramName != null, take paramName
  • *
  • Otherwise, take the string value of value
  • *
*/ private static String name(Object value, String paramName) { return paramName == null ? String.valueOf(value) : paramName; } // ------------------------------------------------------------------------ // XXX: Field API // ------------------------------------------------------------------------ @Override public final void toSQL(RenderContext context) { // Casting can be enforced or prevented switch (context.castMode()) { case NEVER: toSQL(context, getValue(), getType()); return; case ALWAYS: toSQLCast(context); return; case SOME: // This dialect must cast if (context.cast()) { toSQLCast(context); } // In some cases, we should still cast else if (shouldCast(context)) { toSQLCast(context); } else { toSQL(context, getValue(), getType()); } return; } // See if we "should" cast, to stay on the safe side if (shouldCast(context)) { toSQLCast(context); } // Most RDBMS can infer types for bind values else { toSQL(context, getValue(), getType()); } } private boolean shouldCast(RenderContext context) { // In default mode, casting is only done when parameters are NOT inlined if (!isInline(context)) { // Generated enums should not be cast... if (!(getValue() instanceof EnumType)) { switch (context.getDialect()) { // These dialects can hardly detect the type of a bound constant. case DB2: case DERBY: case FIREBIRD: // These dialects have some trouble, when they mostly get it right. case H2: case HSQLDB: // [#1261] There are only a few corner-cases, where this is // really needed. Check back on related CUBRID bugs case CUBRID: // [#1029] Postgres and [#632] Sybase need explicit casting // in very rare cases. case POSTGRES: case SYBASE: { 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 (getDataType().isInterval()) { switch (context.getDialect()) { case ORACLE: case POSTGRES: return true; } } return false; } /** * Render the bind variable including a cast, if necessary */ private void toSQLCast(RenderContext context) { DataType dataType = getDataType(context); DataType type = dataType.getSQLDataType(); SQLDialect dialect = context.getDialect(); // [#822] Some RDBMS need precision / scale information on BigDecimals if (getValue() != null && getType() == BigDecimal.class && asList(CUBRID, DB2, DERBY, FIREBIRD, HSQLDB).contains(dialect)) { // Add precision / scale on BigDecimals int scale = ((BigDecimal) getValue()).scale(); int precision = scale + ((BigDecimal) getValue()).precision(); // Firebird's max precision is 18 if (dialect == FIREBIRD) { precision = Math.min(precision, 18); } toSQLCast(context, dataType, 0, precision, scale); } // [#1028] Most databases don't know an OTHER type (except H2, HSQLDB). else if (SQLDataType.OTHER == type) { // If the bind value is set, it can be used to derive the cast type if (value != null) { toSQLCast(context, DefaultDataType.getDataType(dialect, value.getClass()), 0, 0, 0); } // [#632] [#722] Current integration tests show that Ingres and // Sybase can do without casting in most cases. else if (asList(INGRES, SYBASE).contains(dialect)) { context.sql(getBindVariable(context)); } // 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(context, DefaultDataType.getDataType(dialect, String.class), 0, 0, 0); } } // [#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 // [#1130] TODO type can be null for ARRAY types, etc. else if (dialect == POSTGRES && (type == null || !type.isTemporal())) { toSQL(context, getValue(), getType()); } // [#1727] VARCHAR types should be cast to their actual lengths in some // dialects else if ((type == SQLDataType.VARCHAR || type == SQLDataType.CHAR) && asList(FIREBIRD).contains(dialect)) { toSQLCast(context, dataType, getValueLength(), 0, 0); } // In all other cases, the bind variable can be cast normally else { toSQLCast(context, dataType, dataType.length(), dataType.precision(), dataType.scale()); } } private int getValueLength() { String string = (String) getValue(); 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 void toSQLCast(RenderContext context, DataType type, int length, int precision, int scale) { context.keyword("cast("); toSQL(context, getValue(), getType()); context.keyword(" as ") .sql(type.length(length).precision(precision, scale).getCastTypeName(context)) .sql(")"); } /** * Get a bind variable, depending on value of * {@link RenderContext#namedParams()} */ private final String getBindVariable(RenderContext context) { if (context.namedParams()) { int index = context.nextIndex(); if (StringUtils.isBlank(getParamName())) { return ":" + index; } else { return ":" + getName(); } } else { return "?"; } } /** * Inlining abstraction */ private void toSQL(RenderContext context, Object val) { if (val == null) { toSQL(context, val, Object.class); } else { toSQL(context, val, val.getClass()); } } /** * Inlining abstraction */ @SuppressWarnings({ "unchecked", "rawtypes" }) private void toSQL(RenderContext context, Object val, Class type) { SQLDialect dialect = context.getDialect(); // [#650] Check first, if we have a converter for the supplied type Converter converter = DataTypes.converter(type); if (converter != null) { val = ((Converter) converter).to(val); type = converter.fromType(); } if (isInline(context)) { if (val == null) { context.keyword("null"); } else if (type == Boolean.class) { // [#1153] Some dialects don't support boolean literals // TRUE and FALSE if (asList(ASE, DB2, FIREBIRD, ORACLE, SQLSERVER, SQLITE, SYBASE).contains(dialect)) { context.sql(((Boolean) val) ? "1" : "0"); } else { context.keyword(val.toString()); } } // [#1154] Binary data cannot always be inlined else if (type == byte[].class) { byte[] binary = (byte[]) val; if (asList(ASE, SQLSERVER, SYBASE).contains(dialect)) { context.sql("0x") .sql(convertBytesToHex(binary)); } else if (dialect == DB2) { context.keyword("blob") .sql("(X'") .sql(convertBytesToHex(binary)) .sql("')"); } else if (asList(DERBY, H2, HSQLDB, INGRES, MYSQL, SQLITE).contains(dialect)) { context.sql("X'") .sql(convertBytesToHex(binary)) .sql("'"); } else if (asList(ORACLE).contains(dialect)) { context.keyword("hextoraw('") .sql(convertBytesToHex(binary)) .sql("')"); } else if (dialect == POSTGRES) { context.sql("E'") .sql(convertBytesToPostgresOctal(binary)) .keyword("'::bytea"); } // This default behaviour is used in debug logging for dialects // that do not support inlining binary data else { context.sql("X'") .sql(convertBytesToHex(binary)) .sql("'"); } } // Interval extends Number, so let Interval come first! else if (Interval.class.isAssignableFrom(type)) { context.sql("'") .sql(val.toString()) .sql("'"); } else if (Number.class.isAssignableFrom(type)) { context.sql(val.toString()); } // [#1156] Date/Time data types should be inlined using JDBC // escape syntax else if (type == Date.class) { // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement date literals if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) { context.sql("'").sql(val.toString()).sql("'"); } // [#1253] Derby doesn't support the standard literal else if (dialect == DERBY) { context.keyword("date('").sql(val.toString()).sql("')"); } // Most dialects implement SQL standard date literals else { context.keyword("date '").sql(val.toString()).sql("'"); } } else if (type == Timestamp.class) { // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement timestamp literals if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) { context.sql("'").sql(val.toString()).sql("'"); } // [#1253] Derby doesn't support the standard literal else if (dialect == DERBY) { context.keyword("timestamp('").sql(val.toString()).sql("')"); } // CUBRID timestamps have no fractional seconds else if (dialect == CUBRID) { context.keyword("datetime '").sql(val.toString()).sql("'"); } // Most dialects implement SQL standard timestamp literals else { context.keyword("timestamp '").sql(val.toString()).sql("'"); } } else if (type == Time.class) { // The SQLite JDBC driver does not implement the escape syntax // [#1253] SQL Server and Sybase do not implement time literals if (asList(ASE, SQLITE, SQLSERVER, SYBASE).contains(dialect)) { context.sql("'").sql(val.toString()).sql("'"); } // [#1253] Derby doesn't support the standard literal else if (dialect == DERBY) { context.keyword("time('").sql(val.toString()).sql("')"); } // [#1253] Oracle doesn't know time literals else if (dialect == ORACLE) { context.keyword("timestamp '1970-01-01 ").sql(val.toString()).sql("'"); } // Most dialects implement SQL standard time literals else { context.keyword("time '").sql(val.toString()).sql("'"); } } else if (type.isArray()) { // H2 renders arrays as rows if (dialect == H2) { context.sql(Arrays.toString((Object[]) val).replaceAll("\\[([^]]*)\\]", "($1)")); } // By default, render HSQLDB / POSTGRES syntax else { context.keyword("ARRAY") .sql(Arrays.toString((Object[]) val)); } } else if (ArrayRecord.class.isAssignableFrom(type)) { context.sql(val.toString()); } else if (EnumType.class.isAssignableFrom(type)) { toSQL(context, ((EnumType) val).getLiteral()); } else if (UDTRecord.class.isAssignableFrom(type)) { context.sql("[UDT]"); } // Known fall-through types: // - Blob, Clob (both not supported by jOOQ) // - String else { context.sql("'") .sql(val.toString().replace("'", "''")) .sql("'"); } } // In Postgres, some additional casting must be done in some cases... // TODO: Improve this implementation with [#215] (cast support) else if (dialect == SQLDialect.POSTGRES) { // Postgres needs explicit casting for array types if (type.isArray() && byte[].class != type) { context.sql(getBindVariable(context)); context.sql("::"); context.keyword(DefaultDataType.getDataType(dialect, type).getCastTypeName(context)); } // ... and also for enum types else if (EnumType.class.isAssignableFrom(type)) { context.sql(getBindVariable(context)); // [#968] Don't cast "synthetic" enum types (note, val can be null!) String name = ((EnumType) type.getEnumConstants()[0]).getName(); if (!StringUtils.isBlank(name)) { context.sql("::"); context.literal(name); } } else { context.sql(getBindVariable(context)); } } else { context.sql(getBindVariable(context)); } } @Override public final void bind(BindContext context) { // [#1302] Bind value only if it was not explicitly forced to be inlined if (!isInline()) { context.bindValue(getValue(), getType()); } } // ------------------------------------------------------------------------ // XXX: Param API // ------------------------------------------------------------------------ @Override public final void setValue(T value) { setConverted(value); } @Override public final void setConverted(Object value) { this.value = getDataType().convert(value); } @Override public final T getValue() { return value; } @Override public final String getParamName() { return paramName; } @Override public final void setInline(boolean inline) { this.inline = inline; } @Override public final boolean isInline() { return inline; } private final boolean isInline(RenderContext context) { return isInline() || context.inline(); } /** * 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); } /** * Postgres uses octals instead of hex encoding */ private static final String convertBytesToPostgresOctal(byte[] binary) { StringBuilder sb = new StringBuilder(); for (byte b : binary) { sb.append("\\\\"); sb.append(leftPad(toOctalString(b), 3, '0')); } return sb.toString(); } }