[#566] Add support for INTERVAL data types - some fixes

[#585] Add support for DATE, TIME and INTERVAL arithmetic - some fixes
This commit is contained in:
Lukas Eder 2012-04-07 12:01:06 +00:00
parent d1285173f7
commit d0d0198cbc
10 changed files with 332 additions and 172 deletions

View File

@ -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<Integer> {
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<Integer> {
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;

View File

@ -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

View File

@ -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<T> extends AbstractFunction<T> {
@ -152,10 +155,10 @@ class Expression<T> extends AbstractFunction<T> {
// 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<T> extends AbstractFunction<T> {
return (Field<Number>) rhs.get(0);
}
@SuppressWarnings("unchecked")
private final YearToMonth rhsAsYTM() {
try {
return ((Param<YearToMonth>) 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<DayToSecond>) 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<Interval>) 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<T> {
/**
@ -226,71 +259,56 @@ class Expression<T> extends AbstractFunction<T> {
*/
private final Field<T> 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<YearToMonth>) 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<DayToSecond>) rhs.get(0)).getValue();
Field<Timestamp> 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<YearToMonth>) 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<DayToSecond>) rhs.get(0)).getValue();
// DB2 needs this cast if lhs is of type DATE.
DataType<T> 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<T> extends AbstractFunction<T> {
case DERBY:
case HSQLDB: {
if (rhs.get(0).getType() == YearToMonth.class) {
YearToMonth interval = ((Param<YearToMonth>) 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<DayToSecond>) 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<org.jooq.types.Interval<?>>) 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<org.jooq.types.Interval<?>>) 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<T> extends AbstractFunction<T> {
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<T> extends AbstractFunction<T> {
// 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<T> type = lhs.getDataType();
if (type.getType() == Date.class) {
return new DefaultExpression();
}
else {
return new Expression(operator, lhs.cast(Date.class), rhsAsNumber());
return new Expression<Date>(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

View File

@ -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<Integer> {
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<Integer> {
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<Integer> {
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);
}

View File

@ -2718,6 +2718,7 @@ public class Factory implements FactoryOperations {
* <p>
* This has been observed to work with the following databases:
* <ul>
* <li>CUBRID (simulated using the GROUP BY .. WITH ROLLUP clause)</li>
* <li>DB2</li>
* <li>MySQL (simulated using the GROUP BY .. WITH ROLLUP clause)</li>
* <li>Oracle</li>
@ -4258,6 +4259,7 @@ public class Factory implements FactoryOperations {
* <li> {@link SQLDialect#H2}: Using <code>GROUP_CONCAT()</code></li>
* <li> {@link SQLDialect#HSQLDB}: Using <code>GROUP_CONCAT()</code></li>
* <li> {@link SQLDialect#MYSQL}: Using <code>GROUP_CONCAT()</code></li>
* <li> {@link SQLDialect#POSTGRES}: Using <code>STRING_AGG()</code></li>
* <li> {@link SQLDialect#SYBASE}: Using <code>LIST()</code></li>
* </ul>
*
@ -4279,6 +4281,7 @@ public class Factory implements FactoryOperations {
* <li> {@link SQLDialect#H2}: Using <code>GROUP_CONCAT</code></li>
* <li> {@link SQLDialect#HSQLDB}: Using <code>GROUP_CONCAT</code></li>
* <li> {@link SQLDialect#MYSQL}: Using <code>GROUP_CONCAT</code></li>
* <li> {@link SQLDialect#POSTGRES}: Using <code>STRING_AGG()</code></li>
* <li> {@link SQLDialect#SYBASE}: Using <code>LIST()</code></li>
* </ul>
*
@ -4305,6 +4308,7 @@ public class Factory implements FactoryOperations {
* <ul>
* <li> {@link SQLDialect#DB2}: Using <code>XMLAGG()</code></li>
* <li> {@link SQLDialect#ORACLE}: Using <code>LISTAGG()</code></li>
* <li> {@link SQLDialect#POSTGRES}: Using <code>STRING_AGG()</code></li>
* <li> {@link SQLDialect#SYBASE}: Using <code>LIST()</code></li>
* </ul>
*
@ -4329,6 +4333,7 @@ public class Factory implements FactoryOperations {
* It is simulated by the following dialects:
* <ul>
* <li> {@link SQLDialect#SYBASE}: Using <code>LIST()</code></li>
* <li> {@link SQLDialect#POSTGRES}: Using <code>STRING_AGG()</code></li>
* </ul>
*
* @see #listAgg(Field)

View File

@ -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<DayToSecond> {
@Override
final Field<DayToSecond> 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<Double> days = function("datediff", SQLDataType.DOUBLE, literal("day"), timestamp2, timestamp1);
Field<Double> 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<Integer> days = field("{datediff}(day, {0}, {1})", INTEGER, timestamp2, timestamp1);
// The intra-day difference in number of milliseconds
Field<Integer> 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;

View File

@ -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 <code>INTERVAL YEAR TO MONTH</code>
* An implementation for the SQL standard <code>INTERVAL DAY TO SECOND</code>
* data type.
* <p>
* <code>DayToSecond</code> 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.
* <p>
* 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 <code>INTERVAL</code> data types are:
* <ul>
* <li> {@link SQLDialect#HSQLDB}</li>
* <li> {@link SQLDialect#INGRES}</li>
* <li> {@link SQLDialect#ORACLE}</li>
* <li> {@link SQLDialect#POSTGRES}</li>
* </ul>
* <p>
* These dialects have been observed to partially support <code>INTERVAL</code>
* data types in date time arithmetic functions, such as
* <code>TIMESTAMPADD</code>, and <code>TIMESTAMPDIFF</code>:
* <ul>
* <li> {@link SQLDialect#CUBRID}</li>
* <li> {@link SQLDialect#MYSQL}</li>
* </ul>
*
* @author Lukas Eder
* @see Interval
*/
public final class DayToSecond extends Number implements Interval<DayToSecond> {
public final class DayToSecond extends Number implements Interval, Comparable<DayToSecond> {
/**
* Generated UID
@ -169,21 +191,21 @@ public final class DayToSecond extends Number implements Interval<DayToSecond> {
/**
* Load a {@link Double} representation of a <code>INTERVAL DAY TO SECOND</code>
*
* @param days The number of days as a fractional number
* @param milli The number of milliseconds as a fractional number
* @return The loaded <code>INTERVAL DAY TO SECOND</code> 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<DayToSecond> {
@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<DayToSecond> {
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<DayToSecond> {
* 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
// -------------------------------------------------------------------------

View File

@ -101,30 +101,46 @@ import org.jooq.SQLDialect;
* </table>
* <p>
* 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:
* <p>
* 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 <code>INTERVAL</code> data types are:
* <ul>
* <li> {@link SQLDialect#HSQLDB}</li>
* <li> {@link SQLDialect#INGRES}</li>
* <li> {@link SQLDialect#ORACLE}</li>
* <li> {@link SQLDialect#POSTGRES}</li>
* </ul>
* <p>
* 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 <code>INTERVAL</code>
* data types in date time arithmetic functions, such as
* <code>TIMESTAMPADD</code>, and <code>TIMESTAMPDIFF</code>:
* <ul>
* <li> {@link SQLDialect#CUBRID}</li>
* <li> {@link SQLDialect#MYSQL}</li>
* </ul>
*
* @author Lukas Eder
*/
public interface Interval<T extends Interval<T>> extends Serializable, Comparable<T> {
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 <code>1</code> for positive or zero, <code>-1</code> for negative
*/
int getSign();
/**
* @see Number#doubleValue()

View File

@ -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 <code>INTERVAL YEAR TO MONTH</code>
* data type.
* <p>
* <code>YearToMonth</code> is a {@link Number} whose {@link Number#intValue()}
* represents the number of months of the interval.
* <p>
* 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 <code>INTERVAL</code> data types are:
* <ul>
* <li> {@link SQLDialect#HSQLDB}</li>
* <li> {@link SQLDialect#INGRES}</li>
* <li> {@link SQLDialect#ORACLE}</li>
* <li> {@link SQLDialect#POSTGRES}</li>
* </ul>
* <p>
* These dialects have been observed to partially support <code>INTERVAL</code>
* data types in date time arithmetic functions, such as
* <code>TIMESTAMPADD</code>, and <code>TIMESTAMPDIFF</code>:
* <ul>
* <li> {@link SQLDialect#CUBRID}</li>
* <li> {@link SQLDialect#MYSQL}</li>
* </ul>
*
* @author Lukas Eder
* @see Interval
*/
public final class YearToMonth extends Number implements Interval<YearToMonth> {
public final class YearToMonth extends Number implements Interval, Comparable<YearToMonth> {
/**
* Generated UID
@ -134,6 +156,11 @@ public final class YearToMonth extends Number implements Interval<YearToMonth> {
return months;
}
@Override
public final int getSign() {
return negative ? -1 : 1;
}
// -------------------------------------------------------------------------
// XXX Number API
// -------------------------------------------------------------------------

View File

@ -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 <I extends Number & Interval<I>> void intervalChecks(Number expected, I interval) {
if (expected.doubleValue() > 1 / 24.0) {
assertEquals(expected.doubleValue(), interval.doubleValue());
assertEquals(expected.floatValue(), interval.floatValue());
}
private <I extends Number & Interval> 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());
}
}
}