Skip to content

Commit 1260a29

Browse files
committed
Merge branch '50-stable' into 51-stable
2 parents 9713e5e + 906f322 commit 1260a29

File tree

4 files changed

+203
-48
lines changed

4 files changed

+203
-48
lines changed

src/java/arjdbc/jdbc/RubyJdbcConnection.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2687,7 +2687,7 @@ protected String internedTypeFor(final ThreadContext context, final IRubyObject
26872687
}
26882688

26892689
protected final RubyTime timeInDefaultTimeZone(final ThreadContext context, final IRubyObject value) {
2690-
return timeInDefaultTimeZone(context, toTime(context, value));
2690+
return timeInDefaultTimeZone(context, DateTimeUtils.toTime(context, value));
26912691
}
26922692

26932693
protected final RubyTime timeInDefaultTimeZone(final ThreadContext context, final RubyTime time) {
@@ -2706,10 +2706,7 @@ protected final DateTime dateTimeInDefaultTimeZone(final ThreadContext context,
27062706
}
27072707

27082708
public static RubyTime toTime(final ThreadContext context, final IRubyObject value) {
2709-
if ( ! ( value instanceof RubyTime ) ) { // unlikely
2710-
return (RubyTime) TypeConverter.convertToTypeWithCheck(value, context.runtime.getTime(), "to_time");
2711-
}
2712-
return (RubyTime) value;
2709+
return DateTimeUtils.toTime(context, value);
27132710
}
27142711

27152712
protected boolean isDefaultTimeZoneUTC(final ThreadContext context) {
@@ -2809,7 +2806,7 @@ protected void setTimestampParameter(final ThreadContext context,
28092806
final int index, IRubyObject value,
28102807
final IRubyObject attribute, final int type) throws SQLException {
28112808

2812-
final RubyTime timeValue = toTime(context, value);
2809+
final RubyTime timeValue = DateTimeUtils.toTime(context, value);
28132810
final DateTime dateTime = dateTimeInDefaultTimeZone(context, timeValue.getDateTime());
28142811
final Timestamp timestamp = new Timestamp(dateTime.getMillis());
28152812
// 1942-11-30T01:02:03.123_456
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package arjdbc.postgresql;
2+
3+
import arjdbc.util.DateTimeUtils;
4+
import org.joda.time.DateTimeZone;
5+
import org.jruby.RubyArray;
6+
import org.jruby.RubyFloat;
7+
import org.jruby.runtime.ThreadContext;
8+
import org.jruby.runtime.builtin.IRubyObject;
9+
10+
/**
11+
* PostgreSQL specific DateTime/Timestamp helpers
12+
* @author dritz
13+
*/
14+
public abstract class PgDateTimeUtils extends DateTimeUtils {
15+
/**
16+
* Convert a ruby value to a PostgreSQL timestamp string
17+
* @param context
18+
* @param value Ruby value, typically a Time instance
19+
* @param zone DateTimeZone to adjust to, optional
20+
* @param withZone include timezone in string?
21+
* @return A string fit for PostgreSQL
22+
*/
23+
public static String timestampValueToString(final ThreadContext context, IRubyObject value, DateTimeZone zone,
24+
boolean withZone) {
25+
if (value instanceof RubyFloat) {
26+
final double dv = ((RubyFloat) value).getValue();
27+
if (dv == Double.POSITIVE_INFINITY) {
28+
return "infinity";
29+
} else if (dv == Double.NEGATIVE_INFINITY) {
30+
return "-infinity";
31+
}
32+
}
33+
return timestampTimeToString(context, value, zone, withZone);
34+
}
35+
36+
/**
37+
* Converts a RubyArray with timestamp values to a java array of PostgreSQL timestamp strings
38+
* @param context
39+
* @param rubyArray
40+
* @return Array of timestamp strings
41+
*/
42+
public static String[] timestampStringArray(final ThreadContext context, final RubyArray rubyArray) {
43+
int size = rubyArray.size();
44+
String[] values = new String[size];
45+
46+
for (int i = 0; i < size; i++) {
47+
IRubyObject elem = rubyArray.eltInternal(i);
48+
values[i] = timestampValueToString(context, elem, DateTimeZone.UTC, false);
49+
}
50+
return values;
51+
}
52+
}

src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,30 @@ protected IRubyObject mapQueryResult(final ThreadContext context, final Connecti
282282
return mapExecuteResult(context, connection, resultSet).toARResult(context);
283283
}
284284

285+
@Override
286+
protected void setArrayParameter(final ThreadContext context,
287+
final Connection connection, final PreparedStatement statement,
288+
final int index, final IRubyObject value,
289+
final IRubyObject attribute, final int type) throws SQLException {
290+
291+
final String typeName = resolveArrayBaseTypeName(context, attribute);
292+
final RubyArray valueForDB = (RubyArray) value.callMethod(context, "values");
293+
294+
Object[] values;
295+
switch (typeName) {
296+
case "datetime":
297+
case "timestamp": {
298+
values = PgDateTimeUtils.timestampStringArray(context, valueForDB);
299+
break;
300+
}
301+
default:
302+
values = valueForDB.toArray();
303+
break;
304+
}
305+
306+
statement.setArray(index, connection.createArrayOf(typeName, values));
307+
}
308+
285309
@Override
286310
protected void setBlobParameter(final ThreadContext context,
287311
final Connection connection, final PreparedStatement statement,
@@ -305,47 +329,9 @@ protected void setTimestampParameter(final ThreadContext context,
305329
final Connection connection, final PreparedStatement statement,
306330
final int index, IRubyObject value,
307331
final IRubyObject attribute, final int type) throws SQLException {
308-
309-
if ( value instanceof RubyFloat ) {
310-
final double doubleValue = ( (RubyFloat) value ).getValue();
311-
if ( Double.isInfinite(doubleValue) ) {
312-
setTimestampInfinity(statement, index, doubleValue);
313-
return;
314-
}
315-
}
316-
317-
RubyTime timeValue = toTime(context, value);
318-
319-
final Timestamp timestamp;
320-
321-
if (timeValue.getDateTime().getYear() > 0) {
322-
timeValue = timeInDefaultTimeZone(context, timeValue);
323-
DateTime dateTime = timeValue.getDateTime();
324-
timestamp = new Timestamp(dateTime.getMillis());
325-
326-
if (timeValue.getNSec() > 0) timestamp.setNanos((int) (timestamp.getNanos() + timeValue.getNSec()));
327-
328-
statement.setTimestamp(index, timestamp, getCalendar(dateTime.getZone()));
329-
}
330-
else {
331-
setTimestampBC(statement, index, timeValue);
332-
}
333-
}
334-
335-
private static void setTimestampBC(final PreparedStatement statement,
336-
final int index, final RubyTime timeValue) throws SQLException {
337-
DateTime dateTime = timeValue.getDateTime();
338-
@SuppressWarnings("deprecated")
339-
Timestamp timestamp = new Timestamp(dateTime.getYear() - 1900,
340-
dateTime.getMonthOfYear() - 1,
341-
dateTime.getDayOfMonth(),
342-
dateTime.getHourOfDay(),
343-
dateTime.getMinuteOfHour(),
344-
dateTime.getSecondOfMinute(),
345-
dateTime.getMillisOfSecond() * 1_000_000 + (int) timeValue.getNSec()
346-
);
347-
348-
statement.setObject(index, timestamp);
332+
// PGJDBC uses strings internally anyway, so using Timestamp doesn't do any good
333+
String tsString = PgDateTimeUtils.timestampValueToString(context, value, null, true);
334+
statement.setObject(index, tsString, Types.OTHER);
349335
}
350336

351337
private static void setTimestampInfinity(final PreparedStatement statement,
@@ -367,7 +353,8 @@ protected void setTimeParameter(final ThreadContext context,
367353
final int index, IRubyObject value,
368354
final IRubyObject attribute, final int type) throws SQLException {
369355
// to handle more fractional second precision than (default) 59.123 only
370-
super.setTimestampParameter(context, connection, statement, index, value, attribute, type);
356+
String timeStr = DateTimeUtils.timeString(context, value, DateTimeZone.UTC, true);
357+
statement.setObject(index, timeStr, Types.OTHER);
371358
}
372359

373360
@Override

src/java/arjdbc/util/DateTimeUtils.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.sql.Timestamp;
2929
import java.util.TimeZone;
3030

31+
import org.joda.time.Chronology;
3132
import org.joda.time.DateTime;
3233
import org.joda.time.DateTimeZone;
3334
import org.joda.time.chrono.ISOChronology;
@@ -40,6 +41,7 @@
4041
import org.jruby.runtime.ThreadContext;
4142
import org.jruby.runtime.builtin.IRubyObject;
4243
import org.jruby.util.ByteList;
44+
import org.jruby.util.TypeConverter;
4345

4446
import static arjdbc.util.StringHelper.decByte;
4547

@@ -48,6 +50,17 @@
4850
* @author kares
4951
*/
5052
public abstract class DateTimeUtils {
53+
public static RubyTime toTime(final ThreadContext context, final IRubyObject value) {
54+
if (!(value instanceof RubyTime)) { // unlikely
55+
return (RubyTime) TypeConverter.convertToTypeWithCheck(value, context.runtime.getTime(), "to_time");
56+
}
57+
return (RubyTime) value;
58+
}
59+
60+
public static DateTime dateTimeInZone(final DateTime dateTime, final DateTimeZone zone) {
61+
if (zone == dateTime.getZone()) return dateTime;
62+
return dateTime.withZone(zone);
63+
}
5164

5265
@SuppressWarnings("deprecation")
5366
public static ByteList timeToString(final Time time) {
@@ -577,4 +590,110 @@ private static int extractIntValue(final CharSequence str, int beg, int end) {
577590
return n;
578591
}
579592

593+
594+
private static final char[] ZEROS = {'0', '0', '0', '0', '0', '0'};
595+
private static final char[][] NUMBERS;
596+
597+
static {
598+
// maximum value is 60 (seconds)
599+
NUMBERS = new char[60][];
600+
for (int i = 0; i < NUMBERS.length; i++) {
601+
NUMBERS[i] = ((i < 10 ? "0" : "") + Integer.toString(i)).toCharArray();
602+
}
603+
}
604+
605+
/**
606+
* Converts a ruby timestamp to a java string, optionally with timezone and timezone adjustment
607+
* @param context
608+
* @param value the ruby value, typically a Time
609+
* @param zone DateTimeZone to adjust to, optional
610+
* @param withZone include timezone in the string?
611+
* @return timestamp as string
612+
*/
613+
public static String timestampTimeToString(final ThreadContext context,
614+
final IRubyObject value, DateTimeZone zone, boolean withZone) {
615+
RubyTime timeValue = toTime(context, value);
616+
DateTime dt = timeValue.getDateTime();
617+
618+
StringBuilder sb = new StringBuilder(36);
619+
620+
int year = dt.getYear();
621+
if (year <= 0) {
622+
year--;
623+
} else if (zone != null) {
624+
dt = dateTimeInZone(dt, zone);
625+
year = dt.getYear();
626+
}
627+
628+
Chronology chrono = dt.getChronology();
629+
long millis = dt.getMillis();
630+
631+
// always use 4 digits for year to avoid short dates being misinterpreted
632+
sb.append(Math.abs(year));
633+
int lead = 4 - sb.length();
634+
if (lead > 0) sb.insert(0, ZEROS, 0, lead);
635+
sb.append('-');
636+
sb.append(NUMBERS[chrono.monthOfYear().get(millis)]);
637+
sb.append('-');
638+
sb.append(NUMBERS[chrono.dayOfMonth().get(millis)]);
639+
if (year < 0) sb.append(" BC");
640+
sb.append(' ');
641+
642+
appendTime(sb, chrono, millis, (int) timeValue.getUSec(), withZone);
643+
644+
return sb.toString();
645+
}
646+
647+
/**
648+
* Converts a ruby time to a java string, optionally with timezone and timezone adjustment
649+
* @param context
650+
* @param value the ruby value, typically a Time
651+
* @param zone DateTimeZone to adjust to, optional
652+
* @param withZone include timezone in the string?
653+
* @return time as string
654+
*/
655+
public static String timeString(final ThreadContext context,
656+
final IRubyObject value, DateTimeZone zone, boolean withZone) {
657+
StringBuilder sb = new StringBuilder(21);
658+
RubyTime timeValue = toTime(context, value);
659+
DateTime dt = timeValue.getDateTime();
660+
if (zone != null) dt = dateTimeInZone(dt, zone);
661+
662+
appendTime(sb, dt.getChronology(), dt.getMillis(), (int) timeValue.getUSec(), withZone);
663+
return sb.toString();
664+
}
665+
666+
private static void appendTime(StringBuilder sb, Chronology chrono,
667+
long millis, int usec, boolean withZone) {
668+
sb.append(NUMBERS[chrono.hourOfDay().get(millis)]);
669+
sb.append(':');
670+
sb.append(NUMBERS[chrono.minuteOfHour().get(millis)]);
671+
sb.append(':');
672+
sb.append(NUMBERS[chrono.secondOfMinute().get(millis)]);
673+
674+
// PG has microsecond resolution. Change when nanos are required
675+
int micros = chrono.millisOfSecond().get(millis) * 1000 + usec;
676+
if (micros > 0) {
677+
sb.append('.');
678+
679+
int len = sb.length();
680+
sb.append(micros);
681+
int lead = 6 - (sb.length() - len);
682+
if (lead > 0) sb.insert(len, ZEROS, 0, lead);
683+
684+
for (int end = sb.length() - 1; sb.charAt(end) == '0'; end--) {
685+
sb.setLength(end);
686+
}
687+
}
688+
689+
if (withZone) {
690+
int offset = chrono.getZone().getOffset(millis) / 1000;
691+
int absoff = Math.abs(offset);
692+
int hours = absoff / 3600;
693+
int mins = (absoff - hours * 3600) / 60;
694+
695+
sb.append(offset < 0 ? '-' : '+');
696+
sb.append(NUMBERS[hours]).append(':').append(NUMBERS[mins]);
697+
}
698+
}
580699
}

0 commit comments

Comments
 (0)