diff --git a/jOOQ/src/main/java/org/jooq/impl/DateDiff.java b/jOOQ/src/main/java/org/jooq/impl/DateDiff.java index ea8a2d4c7c..28a841585a 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DateDiff.java +++ b/jOOQ/src/main/java/org/jooq/impl/DateDiff.java @@ -37,7 +37,6 @@ package org.jooq.impl; import static org.jooq.impl.Factory.field; import static org.jooq.impl.Factory.function; -import static org.jooq.impl.Factory.literal; import java.sql.Date; @@ -70,7 +69,7 @@ class DateDiff extends AbstractFunction { case ASE: case SQLSERVER: case SYBASE: - return function("datediff", getDataType(), literal("day"), date2, date1); + return field("{datediff}(day, {0}, {1})", getDataType(), date2, date1); case MYSQL: return function("datediff", getDataType(), date1, date2); @@ -84,14 +83,17 @@ class DateDiff extends AbstractFunction { case H2: case HSQLDB: - return function("datediff", getDataType(), literal("'day'"), date2, date1); + return field("{datediff}('day', {0}, {1})", getDataType(), date2, date1); + + case SQLITE: + return field("({strftime}('%s', {0}) - {strftime}('%s', {1})) / 86400", getDataType(), date1, date2); case CUBRID: case ORACLE: case POSTGRES: - // TODO [#585] This cast shouldn't be necessary - return date1.sub(date2).cast(Integer.class); + // TODO [#585] This cast shouldn't be necessary + return date1.sub(date2).cast(Integer.class); } return null; diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBindContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBindContext.java index df718ce11c..c3c13c5fdd 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBindContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBindContext.java @@ -195,9 +195,6 @@ class DefaultBindContext extends AbstractBindContext { else if (type == Clob.class) { stmt.setClob(nextIndex(), (Clob) value); } - else if (type == Date.class) { - stmt.setDate(nextIndex(), (Date) value); - } else if (type == Double.class) { stmt.setDouble(nextIndex(), (Double) value); } @@ -216,11 +213,32 @@ class DefaultBindContext extends AbstractBindContext { else if (type == String.class) { stmt.setString(nextIndex(), (String) value); } + + // There is potential for trouble when binding date time as such + // ------------------------------------------------------------- + else if (type == Date.class) { + if (dialect == SQLITE) { + stmt.setString(nextIndex(), ((Date) value).toString()); + } + else { + stmt.setDate(nextIndex(), (Date) value); + } + } else if (type == Time.class) { - stmt.setTime(nextIndex(), (Time) value); + if (dialect == SQLITE) { + stmt.setString(nextIndex(), ((Time) value).toString()); + } + else { + stmt.setTime(nextIndex(), (Time) value); + } } else if (type == Timestamp.class) { - stmt.setTimestamp(nextIndex(), (Timestamp) value); + if (dialect == SQLITE) { + stmt.setString(nextIndex(), ((Timestamp) value).toString()); + } + else { + stmt.setTimestamp(nextIndex(), (Timestamp) value); + } } // [#566] Interval data types are best bound as Strings diff --git a/jOOQ/src/main/java/org/jooq/impl/Expression.java b/jOOQ/src/main/java/org/jooq/impl/Expression.java index a7a118ef27..19c92df20f 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Expression.java +++ b/jOOQ/src/main/java/org/jooq/impl/Expression.java @@ -63,7 +63,7 @@ import static org.jooq.impl.Factory.bitOr; import static org.jooq.impl.Factory.bitXor; import static org.jooq.impl.Factory.field; import static org.jooq.impl.Factory.function; -import static org.jooq.impl.Factory.literal; +import static org.jooq.impl.Factory.two; import static org.jooq.impl.Factory.val; import java.sql.Date; @@ -74,12 +74,15 @@ import java.util.List; import org.jooq.Attachable; import org.jooq.BindContext; import org.jooq.Configuration; +import org.jooq.DataType; import org.jooq.Field; import org.jooq.Param; import org.jooq.QueryPart; import org.jooq.RenderContext; import org.jooq.SQLDialect; +import org.jooq.exception.DataTypeException; import org.jooq.types.DayToSecond; +import org.jooq.types.Interval; import org.jooq.types.YearToMonth; class Expression extends AbstractFunction { @@ -152,10 +155,10 @@ class Expression extends AbstractFunction { // Many dialects don't support shifts. Use multiplication/division instead else if (SHL == operator && asList(ASE, DB2, H2, HSQLDB, INGRES, ORACLE, SQLSERVER, SYBASE).contains(dialect)) { - return lhs.mul(Factory.power(literal(2), rhsAsNumber())); + return lhs.mul(Factory.power(two(), rhsAsNumber())); } else if (SHR == operator && asList(ASE, DB2, H2, HSQLDB, INGRES, ORACLE, SQLSERVER, SYBASE).contains(dialect)) { - return lhs.div(Factory.power(literal(2), rhsAsNumber())); + return lhs.div(Factory.power(two(), rhsAsNumber())); } // These operators are not supported in any dialect @@ -200,6 +203,36 @@ class Expression extends AbstractFunction { return (Field) rhs.get(0); } + @SuppressWarnings("unchecked") + private final YearToMonth rhsAsYTM() { + try { + return ((Param) rhs.get(0)).getValue(); + } + catch (ClassCastException e) { + throw new DataTypeException("Cannot perform datetime arithmetic with a non-numeric, non-interval data type on the right hand side of the expression: " + rhs.get(0)); + } + } + + @SuppressWarnings("unchecked") + private final DayToSecond rhsAsDTS() { + try { + return ((Param) rhs.get(0)).getValue(); + } + catch (ClassCastException e) { + throw new DataTypeException("Cannot perform datetime arithmetic with a non-numeric, non-interval data type on the right hand side of the expression: " + rhs.get(0)); + } + } + + @SuppressWarnings("unchecked") + private final Interval rhsAsInterval() { + try { + return ((Param) rhs.get(0)).getValue(); + } + catch (ClassCastException e) { + throw new DataTypeException("Cannot perform datetime arithmetic with a non-numeric, non-interval data type on the right hand side of the expression: " + rhs.get(0)); + } + } + private class DateExpression extends AbstractFunction { /** @@ -226,71 +259,56 @@ class Expression extends AbstractFunction { */ private final Field getIntervalExpression(Configuration configuration) { SQLDialect dialect = configuration.getDialect(); + int sign = (operator == ADD) ? 1 : -1; switch (dialect) { case ASE: case SYBASE: case SQLSERVER: { if (rhs.get(0).getType() == YearToMonth.class) { - YearToMonth interval = ((Param) rhs.get(0)).getValue(); - - if (operator == ADD) { - return function("dateadd", getDataType(), literal("mm"), val(interval.intValue()), lhs); - } - else { - return function("dateadd", getDataType(), literal("mm"), val(-interval.intValue()), lhs); - } + return field("{dateadd}(mm, {0}, {1})", getDataType(), val(sign * rhsAsYTM().intValue()), lhs); } else { // SQL Server needs this cast. - Field result = lhs.cast(Timestamp.class); - DayToSecond interval = ((Param) rhs.get(0)).getValue(); + Field lhsAsTS = lhs.cast(Timestamp.class); + DayToSecond interval = rhsAsDTS(); - if (operator == ADD) { - if (interval.getNano() != 0) { - int micro = interval.getNano() / 1000; - - result = function("dateadd", getDataType(), literal("us"), val(micro), result); - result = function("dateadd", getDataType(), literal("ss"), val((long) interval.getTotalSeconds()), result); - } - else { - result = function("dateadd", getDataType(), literal("ss"), val((long) interval.getTotalSeconds()), result); - } + // Be careful with 32-bit INT arithmetic. Sybase ASE + // may fatally overflow when using micro-second precision + if (interval.getNano() != 0) { + return field("{dateadd}(ss, {0}, {dateadd}(us, {1}, {2}))", getDataType(), + val(sign * (long) interval.getTotalSeconds()), + val(sign * interval.getMicro()), + lhsAsTS); } else { - if (interval.getNano() != 0) { - int micro = interval.getNano() / 1000; - - result = function("dateadd", getDataType(), literal("us"), val(-micro), result); - result = function("dateadd", getDataType(), literal("ss"), val(-(long) interval.getTotalSeconds()), result); - } - else { - result = function("dateadd", getDataType(), literal("ss"), val(-(long) interval.getTotalSeconds()), result); - } + return field("{dateadd}(ss, {0}, {1})", getDataType(), val(sign * (long) interval.getTotalSeconds()), lhsAsTS); } - - return (Field) result; } } case DB2: { if (rhs.get(0).getType() == YearToMonth.class) { - YearToMonth interval = ((Param) rhs.get(0)).getValue(); - if (operator == ADD) { - return lhs.add(field("{0} month", val(interval.intValue()))); + return lhs.add(field("{0} month", val(rhsAsYTM().intValue()))); } else { - return lhs.sub(field("{0} month", val(interval.intValue()))); + return lhs.sub(field("{0} month", val(rhsAsYTM().intValue()))); } } else { - DayToSecond interval = ((Param) rhs.get(0)).getValue(); + // DB2 needs this cast if lhs is of type DATE. + DataType type = lhs.getDataType(); + if (operator == ADD) { - return (Field) lhs.cast(Timestamp.class).add(field("{0} microseconds", val(interval.getTotalMicro()))); + return lhs.cast(Timestamp.class) + .add(field("{0} microseconds", val(rhsAsDTS().getTotalMicro()))) + .cast(type); } else { - return (Field) lhs.cast(Timestamp.class).sub(field("{0} microseconds", val(interval.getTotalMicro()))); + return lhs.cast(Timestamp.class) + .sub(field("{0} microseconds", val(rhsAsDTS().getTotalMicro()))) + .cast(type); } } } @@ -298,60 +316,53 @@ class Expression extends AbstractFunction { case DERBY: case HSQLDB: { if (rhs.get(0).getType() == YearToMonth.class) { - YearToMonth interval = ((Param) rhs.get(0)).getValue(); - - if (operator == ADD) { - return field("{fn {timestampadd}({sql_tsi_month}, {0}, {1}) }", getDataType(), val(interval.intValue()), lhs); - } - else { - return field("{fn {timestampadd}({sql_tsi_month}, {0}, {1}) }", getDataType(), val(-interval.intValue()), lhs); - } + return field("{fn {timestampadd}({sql_tsi_month}, {0}, {1}) }", + getDataType(), val(sign * rhsAsYTM().intValue()), lhs); } else { - DayToSecond interval = ((Param) rhs.get(0)).getValue(); - - if (operator == ADD) { - return field("{fn {timestampadd}({sql_tsi_second}, {0}, {1}) }", getDataType(), val((long) interval.getTotalSeconds()), lhs); - } - else { - return field("{fn {timestampadd}({sql_tsi_second}, {0}, {1}) }", getDataType(), val((long) -interval.getTotalSeconds()), lhs); - } + return field("{fn {timestampadd}({sql_tsi_second}, {0}, {1}) }", + getDataType(), val(sign * (long) rhsAsDTS().getTotalSeconds()), lhs); } } case CUBRID: case MYSQL: { - org.jooq.types.Interval interval = ((Param>) rhs.get(0)).getValue(); + Interval interval = rhsAsInterval(); if (operator == SUBTRACT) { interval = interval.neg(); } if (rhs.get(0).getType() == YearToMonth.class) { - return field("{date_add}({0}, {interval} {1} {year_month})", getDataType(), lhs, val(interval)); + return field("{date_add}({0}, {interval} {1} {year_month})", getDataType(), lhs, val(interval, String.class)); } else { if (dialect == MYSQL) { - return field("{date_add}({0}, {interval} {1} {day_microsecond})", getDataType(), lhs, val(interval)); + return field("{date_add}({0}, {interval} {1} {day_microsecond})", getDataType(), lhs, val(interval, String.class)); } else { - return field("{date_add}({0}, {interval} {1} {day_millisecond})", getDataType(), lhs, val(interval)); + return field("{date_add}({0}, {interval} {1} {day_millisecond})", getDataType(), lhs, val(interval, String.class)); } } } case H2: { - org.jooq.types.Interval interval = ((Param>) rhs.get(0)).getValue(); - - if (operator == SUBTRACT) { - interval = interval.neg(); - } - if (rhs.get(0).getType() == YearToMonth.class) { - return function("dateadd", getDataType(), literal("'month'"), val(interval.intValue()), lhs); + return field("{dateadd}('month', {0}, {1})", getDataType(), val(sign * rhsAsYTM().intValue()), lhs); } else { - return function("dateadd", getDataType(), literal("'ms'"), val((long) ((DayToSecond) interval).getTotalMilli()), lhs); + return field("{dateadd}('ms', {0}, {1})", getDataType(), val(sign * (long) rhsAsDTS().getTotalMilli()), lhs); + } + } + + case SQLITE: { + String prefix = (sign > 0) ? "+" : "-"; + + if (rhs.get(0).getType() == YearToMonth.class) { + return field("{datetime}({0}, '" + prefix + rhsAsYTM().intValue() + " months')", getDataType(), lhs); + } + else { + return field("{datetime}({0}, '" + prefix + rhsAsDTS().getTotalSeconds() + " seconds')", getDataType(), lhs); } } @@ -370,10 +381,10 @@ class Expression extends AbstractFunction { case SQLSERVER: case SYBASE: { if (operator == ADD) { - return function("dateadd", getDataType(), literal("day"), rhsAsNumber(), lhs); + return field("{dateadd}(day, {0}, {1})", getDataType(), rhsAsNumber(), lhs); } else { - return function("dateadd", getDataType(), literal("day"), rhsAsNumber().neg(), lhs); + return field("{dateadd}(day, {0}, {1})", getDataType(), rhsAsNumber().neg(), lhs); } } @@ -409,29 +420,31 @@ class Expression extends AbstractFunction { // Ingres is not working yet case INGRES: { if (operator == ADD) { - return lhs.add(field("date('" + rhsAsNumber() + " days')", Object.class)); + return lhs.add(field("{date}('" + rhsAsNumber() + " days')", Object.class)); } else { - return lhs.sub(field("date('" + rhsAsNumber() + " days')", Object.class)); + return lhs.sub(field("{date}('" + rhsAsNumber() + " days')", Object.class)); } } - // Postgres can add / subtract days using +/- operators only to DATE case POSTGRES: { - if (getType() == Date.class) { + // Postgres can add / subtract days using +/- operators only to DATE + DataType type = lhs.getDataType(); + + if (type.getType() == Date.class) { return new DefaultExpression(); } else { - return new Expression(operator, lhs.cast(Date.class), rhsAsNumber()); + return new Expression(operator, lhs.cast(Date.class), rhsAsNumber()).cast(type); } } case SQLITE: if (operator == ADD) { - return function("datetime", getDataType(), lhs, literal("+" + rhsAsNumber() + " day")); + return field("{datetime}({0}, '+" + rhsAsNumber() + " day')", getDataType(), lhs); } else { - return function("datetime", getDataType(), lhs, literal("-" + rhsAsNumber() + " day")); + return field("{datetime}({0}, '-" + rhsAsNumber() + " day')", getDataType(), lhs); } // These dialects can add / subtract days using +/- operators diff --git a/jOOQ/src/main/java/org/jooq/impl/Extract.java b/jOOQ/src/main/java/org/jooq/impl/Extract.java index 229506f468..f0267ffd9e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Extract.java +++ b/jOOQ/src/main/java/org/jooq/impl/Extract.java @@ -36,10 +36,8 @@ package org.jooq.impl; -import static org.jooq.impl.Factory.field; import static org.jooq.impl.Factory.field; import static org.jooq.impl.Factory.function; -import static org.jooq.impl.Factory.literal; import org.jooq.Configuration; import org.jooq.DatePart; @@ -69,17 +67,17 @@ class Extract extends AbstractFunction { case SQLITE: switch (datePart) { case YEAR: - return function("strftime", SQLDataType.INTEGER, literal("'%Y'"), field); + return field("{strftime}('%Y', {0})", SQLDataType.INTEGER, field); case MONTH: - return function("strftime", SQLDataType.INTEGER, literal("'%m'"), field); + return field("{strftime}('%m', {0})", SQLDataType.INTEGER, field); case DAY: - return function("strftime", SQLDataType.INTEGER, literal("'%d'"), field); + return field("{strftime}('%d', {0})", SQLDataType.INTEGER, field); case HOUR: - return function("strftime", SQLDataType.INTEGER, literal("'%H'"), field); + return field("{strftime}('%H', {0})", SQLDataType.INTEGER, field); case MINUTE: - return function("strftime", SQLDataType.INTEGER, literal("'%M'"), field); + return field("{strftime}('%M', {0})", SQLDataType.INTEGER, field); case SECOND: - return function("strftime", SQLDataType.INTEGER, literal("'%S'"), field); + return field("{strftime}('%S', {0})", SQLDataType.INTEGER, field); default: throw new SQLDialectNotSupportedException("DatePart not supported: " + datePart); } @@ -106,17 +104,17 @@ class Extract extends AbstractFunction { case ORACLE: switch (datePart) { case YEAR: - return function("to_char", SQLDataType.INTEGER, field, literal("'YYYY'")); + return field("{to_char}({0}, 'YYYY')", SQLDataType.INTEGER, field); case MONTH: - return function("to_char", SQLDataType.INTEGER, field, literal("'MM'")); + return field("{to_char}({0}, 'MM')", SQLDataType.INTEGER, field); case DAY: - return function("to_char", SQLDataType.INTEGER, field, literal("'DD'")); + return field("{to_char}({0}, 'DD')", SQLDataType.INTEGER, field); case HOUR: - return function("to_char", SQLDataType.INTEGER, field, literal("'HH24'")); + return field("{to_char}({0}, 'HH24')", SQLDataType.INTEGER, field); case MINUTE: - return function("to_char", SQLDataType.INTEGER, field, literal("'MI'")); + return field("{to_char}({0}, 'MI')", SQLDataType.INTEGER, field); case SECOND: - return function("to_char", SQLDataType.INTEGER, field, literal("'SS'")); + return field("{to_char}({0}, 'SS')", SQLDataType.INTEGER, field); default: throw new SQLDialectNotSupportedException("DatePart not supported: " + datePart); } @@ -126,17 +124,17 @@ class Extract extends AbstractFunction { case SYBASE: switch (datePart) { case YEAR: - return function("datepart", SQLDataType.INTEGER, field("yy"), field); + return field("{datepart}(yy, {0})", SQLDataType.INTEGER, field); case MONTH: - return function("datepart", SQLDataType.INTEGER, field("mm"), field); + return field("{datepart}(mm, {0})", SQLDataType.INTEGER, field); case DAY: - return function("datepart", SQLDataType.INTEGER, field("dd"), field); + return field("{datepart}(dd, {0})", SQLDataType.INTEGER, field); case HOUR: - return function("datepart", SQLDataType.INTEGER, field("hh"), field); + return field("{datepart}(hh, {0})", SQLDataType.INTEGER, field); case MINUTE: - return function("datepart", SQLDataType.INTEGER, field("mi"), field); + return field("{datepart}(mi, {0})", SQLDataType.INTEGER, field); case SECOND: - return function("datepart", SQLDataType.INTEGER, field("ss"), field); + return field("{datepart}(ss, {0})", SQLDataType.INTEGER, field); default: throw new SQLDialectNotSupportedException("DatePart not supported: " + datePart); } diff --git a/jOOQ/src/main/java/org/jooq/impl/Factory.java b/jOOQ/src/main/java/org/jooq/impl/Factory.java index f631b3e769..22e5459867 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Factory.java +++ b/jOOQ/src/main/java/org/jooq/impl/Factory.java @@ -2718,6 +2718,7 @@ public class Factory implements FactoryOperations { *

* This has been observed to work with the following databases: *

    + *
  • CUBRID (simulated using the GROUP BY .. WITH ROLLUP clause)
  • *
  • DB2
  • *
  • MySQL (simulated using the GROUP BY .. WITH ROLLUP clause)
  • *
  • Oracle
  • @@ -4258,6 +4259,7 @@ public class Factory implements FactoryOperations { *
  • {@link SQLDialect#H2}: Using GROUP_CONCAT()
  • *
  • {@link SQLDialect#HSQLDB}: Using GROUP_CONCAT()
  • *
  • {@link SQLDialect#MYSQL}: Using GROUP_CONCAT()
  • + *
  • {@link SQLDialect#POSTGRES}: Using STRING_AGG()
  • *
  • {@link SQLDialect#SYBASE}: Using LIST()
  • *
* @@ -4279,6 +4281,7 @@ public class Factory implements FactoryOperations { *
  • {@link SQLDialect#H2}: Using GROUP_CONCAT
  • *
  • {@link SQLDialect#HSQLDB}: Using GROUP_CONCAT
  • *
  • {@link SQLDialect#MYSQL}: Using GROUP_CONCAT
  • + *
  • {@link SQLDialect#POSTGRES}: Using STRING_AGG()
  • *
  • {@link SQLDialect#SYBASE}: Using LIST()
  • * * @@ -4305,6 +4308,7 @@ public class Factory implements FactoryOperations { *
      *
    • {@link SQLDialect#DB2}: Using XMLAGG()
    • *
    • {@link SQLDialect#ORACLE}: Using LISTAGG()
    • + *
    • {@link SQLDialect#POSTGRES}: Using STRING_AGG()
    • *
    • {@link SQLDialect#SYBASE}: Using LIST()
    • *
    * @@ -4329,6 +4333,7 @@ public class Factory implements FactoryOperations { * It is simulated by the following dialects: *
      *
    • {@link SQLDialect#SYBASE}: Using LIST()
    • + *
    • {@link SQLDialect#POSTGRES}: Using STRING_AGG()
    • *
    * * @see #listAgg(Field) diff --git a/jOOQ/src/main/java/org/jooq/impl/TimestampDiff.java b/jOOQ/src/main/java/org/jooq/impl/TimestampDiff.java index 70ceed5309..175ec0a057 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TimestampDiff.java +++ b/jOOQ/src/main/java/org/jooq/impl/TimestampDiff.java @@ -37,7 +37,7 @@ package org.jooq.impl; import static org.jooq.impl.Factory.field; import static org.jooq.impl.Factory.function; -import static org.jooq.impl.Factory.literal; +import static org.jooq.impl.SQLDataType.INTEGER; import java.sql.Timestamp; @@ -67,47 +67,56 @@ class TimestampDiff extends AbstractFunction { @Override final Field getFunction0(Configuration configuration) { + double milliInDay = new DayToSecond(1).getTotalMilli(); + switch (configuration.getDialect()) { // Sybase ASE's datediff incredibly overflows on 3 days' worth of // microseconds. That's why the days have to be leveled at first case ASE: - Field days = function("datediff", SQLDataType.DOUBLE, literal("day"), timestamp2, timestamp1); - Field milli = function("datediff", SQLDataType.DOUBLE, literal("ms"), timestamp2.add(days), timestamp1); - return (Field) days.add(milli.div(literal(new DayToSecond(1).getTotalMilli()))); + + // The difference in number of days + Field days = field("{datediff}(day, {0}, {1})", INTEGER, timestamp2, timestamp1); + + // The intra-day difference in number of milliseconds + Field milli = field("{datediff}(ms, {0}, {1})", INTEGER, timestamp2.add(days), timestamp1); + return (Field) days.mul(86400000).add(milli); // CUBRID's datetime operations operate on a millisecond level case CUBRID: - return (Field) timestamp1.sub(timestamp2).div(literal(new DayToSecond(1).getTotalMilli())); + return (Field) timestamp1.sub(timestamp2); // Fun with DB2 dates. Find some info here: // http://www.ibm.com/developerworks/data/library/techarticle/0211yip/0211yip3.html case DB2: - return (Field) function("days", SQLDataType.INTEGER, timestamp1).sub( - function("days", SQLDataType.INTEGER, timestamp2)).add( - function("midnight_seconds", SQLDataType.INTEGER, timestamp1).sub( - function("midnight_seconds", SQLDataType.INTEGER, timestamp2)).div(literal(new DayToSecond(1).getTotalSeconds()))); + return (Field) function("days", INTEGER, timestamp1).sub( + function("days", INTEGER, timestamp2)).mul(86400000).add( + function("midnight_seconds", INTEGER, timestamp1).sub( + function("midnight_seconds", INTEGER, timestamp2)).mul(1000)); case DERBY: - return (Field) field("{fn {timestampdiff}({sql_tsi_second}, {0}, {1}) }", SQLDataType.INTEGER, timestamp2, timestamp1).div(literal(new DayToSecond(1).getTotalSeconds())); + return (Field) field("1000 * {fn {timestampdiff}({sql_tsi_second}, {0}, {1}) }", INTEGER, timestamp2, timestamp1); case H2: case HSQLDB: - return function("datediff", getDataType(), literal("'ms'"), timestamp2, timestamp1).div(literal(new DayToSecond(1).getTotalMilli())); + return field("{datediff}('ms', {0}, {1})", getDataType(), timestamp2, timestamp1); // MySQL's datetime operations operate on a microsecond level case MYSQL: - return function("timestampdiff", getDataType(), literal("microsecond"), timestamp2, timestamp1).div(literal(new DayToSecond(1).getTotalMicro())); + return field("{timestampdiff}(microsecond, {0}, {1}) / 1000", getDataType(), timestamp2, timestamp1); case SQLSERVER: case SYBASE: - return function("datediff", getDataType(), literal("ms"), timestamp2, timestamp1).div(literal(new DayToSecond(1).getTotalMilli())); + return field("{datediff}(ms, {0}, {1})", getDataType(), timestamp2, timestamp1); + + case SQLITE: + return field("({strftime}('%s', {0}) - {strftime}('%s', {1})) * 1000", getDataType(), timestamp1, timestamp2); case ORACLE: case POSTGRES: - // TODO [#585] This cast shouldn't be necessary - return timestamp1.sub(timestamp2).cast(DayToSecond.class); + // TODO [#585] This cast shouldn't be necessary + return timestamp1.sub(timestamp2).cast(DayToSecond.class); } return null; diff --git a/jOOQ/src/main/java/org/jooq/types/DayToSecond.java b/jOOQ/src/main/java/org/jooq/types/DayToSecond.java index 311c62a812..9ce4ae2430 100644 --- a/jOOQ/src/main/java/org/jooq/types/DayToSecond.java +++ b/jOOQ/src/main/java/org/jooq/types/DayToSecond.java @@ -38,22 +38,44 @@ package org.jooq.types; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jooq.Field; +import org.jooq.SQLDialect; import org.jooq.tools.Convert; import org.jooq.tools.StringUtils; /** - * An implementation for the SQL standard INTERVAL YEAR TO MONTH + * An implementation for the SQL standard INTERVAL DAY TO SECOND * data type. *

    * DayToSecond is a {@link Number} whose {@link Number#intValue()} - * represents the (truncated) number of days of the interval, - * {@link Number#doubleValue()} represents the approximative number of days - * (including hours, minutes, seconds, nanoseconds) of the interval. + * represents the (truncated) number of milliseconds of the interval, + * {@link Number#doubleValue()} represents the approximative number of + * milliseconds (including hours, minutes, seconds, nanoseconds) of the + * interval. + *

    + * Note: only a few databases actually support this data type on its own. You + * can still use it for date time arithmetic in other databases, though, through + * {@link Field#add(Field)} and {@link Field#sub(Field)} Databases that have + * been observed to natively support INTERVAL data types are: + *

      + *
    • {@link SQLDialect#HSQLDB}
    • + *
    • {@link SQLDialect#INGRES}
    • + *
    • {@link SQLDialect#ORACLE}
    • + *
    • {@link SQLDialect#POSTGRES}
    • + *
    + *

    + * These dialects have been observed to partially support INTERVAL + * data types in date time arithmetic functions, such as + * TIMESTAMPADD, and TIMESTAMPDIFF: + *

      + *
    • {@link SQLDialect#CUBRID}
    • + *
    • {@link SQLDialect#MYSQL}
    • + *
    * * @author Lukas Eder * @see Interval */ -public final class DayToSecond extends Number implements Interval { +public final class DayToSecond extends Number implements Interval, Comparable { /** * Generated UID @@ -169,21 +191,21 @@ public final class DayToSecond extends Number implements Interval { /** * Load a {@link Double} representation of a INTERVAL DAY TO SECOND * - * @param days The number of days as a fractional number + * @param milli The number of milliseconds as a fractional number * @return The loaded INTERVAL DAY TO SECOND object */ - public static DayToSecond valueOf(double days) { - double abs = Math.abs(days); + public static DayToSecond valueOf(double milli) { + double abs = Math.abs(milli); - int d = (int) abs; abs = (abs - d) * 24.0; - int h = (int) abs; abs = (abs - h) * 60.0; - int m = (int) abs; abs = (abs - m) * 60.0; - int s = (int) abs; abs = (abs - s) * 1000000000.0; - int n = (int) abs; + int n = (int) ((abs % 1000) * 1000000.0); abs = Math.floor(abs / 1000); + int s = (int) (abs % 60); abs = Math.floor(abs / 60); + int m = (int) (abs % 60); abs = Math.floor(abs / 60); + int h = (int) (abs % 24); abs = Math.floor(abs / 24); + int d = (int) abs; DayToSecond result = new DayToSecond(d, h, m, s, n); - if (days < 0) { + if (milli < 0) { result = result.neg(); } @@ -211,11 +233,7 @@ public final class DayToSecond extends Number implements Interval { @Override public final double doubleValue() { - return getTotalDays(); - } - - private final int negativeFactor() { - return negative ? -1 : 1; + return getTotalMilli(); } // ------------------------------------------------------------------------- @@ -260,6 +278,20 @@ public final class DayToSecond extends Number implements Interval { return seconds; } + /** + * Get the (truncated) milli-part of this interval + */ + public final int getMilli() { + return nano / 1000000; + } + + /** + * Get the (truncated) micro-part of this interval + */ + public final int getMicro() { + return nano / 1000; + } + /** * Get the nano-part of this interval */ @@ -271,55 +303,91 @@ public final class DayToSecond extends Number implements Interval { * Get the whole interval in days */ public final double getTotalDays() { - return getTotalNano() / 1000000000.0 / 3600.0 / 24.0; + return getSign() * ( + nano / (24.0 * 3600.0 * 1000000000.0) + + seconds / (24.0 * 3600.0) + + minutes / (24.0 * 60.0) + + hours / 24.0 + + days); } /** * Get the whole interval in hours */ public final double getTotalHours() { - return getTotalNano() / 1000000000.0 / 3600.0; + return getSign() * ( + nano / (3600.0 * 1000000000.0) + + seconds / 3600.0 + + minutes / 60.0 + + hours + + 24.0 * days); } /** * Get the whole interval in minutes */ public final double getTotalMinutes() { - return getTotalNano() / 1000000000.0 / 60.0; + return getSign() * ( + nano / (60.0 * 1000000000.0) + + seconds / 60.0 + + minutes + + 60.0 * hours + + 60.0 * 24.0 * days); } /** * Get the whole interval in seconds */ public final double getTotalSeconds() { - return getTotalNano() / 1000000000.0; - } + return getSign() * ( + nano / 1000000000.0 + + seconds + + 60.0 * minutes + + 3600.0 * hours + + 3600.0 * 24.0 * days); + } /** * Get the whole interval in milli-seconds */ public final double getTotalMilli() { - return getTotalNano() / 1000000.0; + return getSign() * ( + nano / 1000000.0 + + 1000.0 * seconds + + 1000.0 * 60.0 * minutes + + 1000.0 * 3600.0 * hours + + 1000.0 * 3600.0 * 24.0 * days); } /** * Get the whole interval in micro-seconds */ public final double getTotalMicro() { - return getTotalNano() / 1000.0; + return getSign() * ( + nano / 1000.0 + + 1000000.0 * seconds + + 1000000.0 * 60.0 * minutes + + 1000000.0 * 3600.0 * hours + + 1000000.0 * 3600.0 * 24.0 * days); } /** * Get the whole interval in nano-seconds */ public final double getTotalNano() { - return negativeFactor() * (nano + + return getSign() * ( + nano + 1000000000.0 * seconds + 1000000000.0 * 60.0 * minutes + 1000000000.0 * 3600.0 * hours + 1000000000.0 * 3600.0 * 24.0 * days); } + @Override + public final int getSign() { + return negative ? -1 : 1; + } + // ------------------------------------------------------------------------- // XXX Comparable and Object API // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/types/Interval.java b/jOOQ/src/main/java/org/jooq/types/Interval.java index 66299f6a39..584d306c62 100644 --- a/jOOQ/src/main/java/org/jooq/types/Interval.java +++ b/jOOQ/src/main/java/org/jooq/types/Interval.java @@ -101,30 +101,46 @@ import org.jooq.SQLDialect; * *

    * Interval implementations can be expected to also also extend {@link Number}. - * True SQL standard INTERVAL data types have been observed to be supported by - * any of these dialects: + *

    + * Note: only a few databases actually support this data type on its own. You + * can still use it for date time arithmetic in other databases, though, through + * {@link Field#add(Field)} and {@link Field#sub(Field)} Databases that have + * been observed to natively support INTERVAL data types are: *

      + *
    • {@link SQLDialect#HSQLDB}
    • *
    • {@link SQLDialect#INGRES}
    • *
    • {@link SQLDialect#ORACLE}
    • *
    • {@link SQLDialect#POSTGRES}
    • *
    *

    - * In other dialects, jOOQ allows for using them for date time arithmetic. See - * {@link Field#add(Field)}, {@link Field#sub(Field)} + * These dialects have been observed to partially support INTERVAL + * data types in date time arithmetic functions, such as + * TIMESTAMPADD, and TIMESTAMPDIFF: + *

      + *
    • {@link SQLDialect#CUBRID}
    • + *
    • {@link SQLDialect#MYSQL}
    • + *
    * * @author Lukas Eder */ -public interface Interval> extends Serializable, Comparable { +public interface Interval extends Serializable { /** * Negate the interval (change its sign) */ - T neg(); + Interval neg(); /** * Get the absolute value of the interval (set its sign to positive) */ - T abs(); + Interval abs(); + + /** + * The sign of the interval + * + * @return 1 for positive or zero, -1 for negative + */ + int getSign(); /** * @see Number#doubleValue() diff --git a/jOOQ/src/main/java/org/jooq/types/YearToMonth.java b/jOOQ/src/main/java/org/jooq/types/YearToMonth.java index fd4abf685c..71a93588af 100644 --- a/jOOQ/src/main/java/org/jooq/types/YearToMonth.java +++ b/jOOQ/src/main/java/org/jooq/types/YearToMonth.java @@ -38,17 +38,39 @@ package org.jooq.types; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jooq.Field; +import org.jooq.SQLDialect; + /** * An implementation for the SQL standard INTERVAL YEAR TO MONTH * data type. *

    * YearToMonth is a {@link Number} whose {@link Number#intValue()} * represents the number of months of the interval. + *

    + * Note: only a few databases actually support this data type on its own. You + * can still use it for date time arithmetic in other databases, though, through + * {@link Field#add(Field)} and {@link Field#sub(Field)} Databases that have + * been observed to natively support INTERVAL data types are: + *

      + *
    • {@link SQLDialect#HSQLDB}
    • + *
    • {@link SQLDialect#INGRES}
    • + *
    • {@link SQLDialect#ORACLE}
    • + *
    • {@link SQLDialect#POSTGRES}
    • + *
    + *

    + * These dialects have been observed to partially support INTERVAL + * data types in date time arithmetic functions, such as + * TIMESTAMPADD, and TIMESTAMPDIFF: + *

      + *
    • {@link SQLDialect#CUBRID}
    • + *
    • {@link SQLDialect#MYSQL}
    • + *
    * * @author Lukas Eder * @see Interval */ -public final class YearToMonth extends Number implements Interval { +public final class YearToMonth extends Number implements Interval, Comparable { /** * Generated UID @@ -134,6 +156,11 @@ public final class YearToMonth extends Number implements Interval { return months; } + @Override + public final int getSign() { + return negative ? -1 : 1; + } + // ------------------------------------------------------------------------- // XXX Number API // ------------------------------------------------------------------------- diff --git a/jOOQ/src/test/java/org/jooq/test/jOOQTest.java b/jOOQ/src/test/java/org/jooq/test/jOOQTest.java index e91617ed98..6af29c26dd 100644 --- a/jOOQ/src/test/java/org/jooq/test/jOOQTest.java +++ b/jOOQ/src/test/java/org/jooq/test/jOOQTest.java @@ -37,6 +37,7 @@ package org.jooq.test; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; import static org.jooq.JoinType.LEFT_OUTER_JOIN; import static org.jooq.impl.Factory.avg; import static org.jooq.impl.Factory.condition; @@ -2288,21 +2289,24 @@ public class jOOQTest { @Test public void testDayToSecond() { + for (double i = -1394892834972.0; i <= 23487289374987.0; i += 283749827.3839293) { + intervalChecks(i, DayToSecond.valueOf(i)); + } + for (int i = 0; i <= 5; i++) { - intervalChecks(i / 2.0, DayToSecond.valueOf(i / 2.0)); - intervalChecks(i, new DayToSecond(i)); - intervalChecks(i / 24.0, new DayToSecond(0, i)); - intervalChecks(i / 24.0 / 60.0, new DayToSecond(0, 0, i)); - intervalChecks(i / 24.0 / 3600.0, new DayToSecond(0, 0, 0, i)); - intervalChecks(i / 24.0 / 3600.0 / 1000000000.0, new DayToSecond(0, 0, 0, 0, i)); + intervalChecks(i * 1000 * 86400.0, new DayToSecond(i)); + intervalChecks(i * 1000 * 3600.0, new DayToSecond(0, i)); + intervalChecks(i * 1000 * 60.0, new DayToSecond(0, 0, i)); + intervalChecks(i * 1000, new DayToSecond(0, 0, 0, i)); + intervalChecks(i / 1000000.0, new DayToSecond(0, 0, 0, 0, i)); } } - private > void intervalChecks(Number expected, I interval) { - if (expected.doubleValue() > 1 / 24.0) { - assertEquals(expected.doubleValue(), interval.doubleValue()); - assertEquals(expected.floatValue(), interval.floatValue()); - } + private void intervalChecks(Number expected, I interval) { + // Allow some floating point arithmetic inaccuracy + assertTrue(Math.abs(Double.doubleToLongBits(expected.doubleValue()) - Double.doubleToLongBits(interval.doubleValue())) < 50); + assertTrue(Math.abs(Float.floatToIntBits(expected.floatValue()) - Float.floatToIntBits(interval.floatValue())) < 5); + assertEquals(expected.byteValue(), interval.byteValue()); assertEquals(expected.shortValue(), interval.shortValue()); assertEquals(expected.intValue(), interval.intValue()); @@ -2316,11 +2320,11 @@ public class jOOQTest { DayToSecond m = DayToSecond.valueOf(interval.toString()); assertEquals(interval, m); assertEquals(m.getDays(), - (int) m.getTotalDays()); + m.getSign() * (int) m.getTotalDays()); assertEquals(m.getDays() * 24 + m.getHours(), - (int) m.getTotalHours()); + m.getSign() * (int) m.getTotalHours()); assertEquals(m.getDays() * 24 * 60 + m.getHours() * 60 + m.getMinutes(), - (int) m.getTotalMinutes()); + m.getSign() * (int) m.getTotalMinutes()); } } }