Skip to content

Commit 2341642

Browse files
committed
feat(postgres): add support for interval type
1 parent e2367ef commit 2341642

File tree

5 files changed

+92
-28
lines changed

5 files changed

+92
-28
lines changed

lib/arjdbc/postgresql/adapter.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def configure_connection
8787
execute("SET time zone '#{tz}'", 'SCHEMA')
8888
end unless redshift?
8989

90+
# Set interval output format to ISO 8601 for ease of parsing by ActiveSupport::Duration.parse
91+
execute("SET intervalstyle = iso_8601", "SCHEMA")
92+
9093
# SET statements from :variables config hash
9194
# http://www.postgresql.org/docs/8.3/static/sql-set.html
9295
(config[:variables] || {}).map do |k, v|

lib/arjdbc/postgresql/oid_types.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ def initialize_type_map(m = type_map)
155155
m.register_type 'polygon', OID::SpecializedString.new(:polygon)
156156
m.register_type 'circle', OID::SpecializedString.new(:circle)
157157

158-
m.register_type 'interval' do |_, _, sql_type|
158+
m.register_type 'interval' do |*args, sql_type|
159159
precision = extract_precision(sql_type)
160-
OID::SpecializedString.new(:interval, precision: precision)
160+
OID::Interval.new(precision: precision)
161161
end
162162

163163
register_class_with_precision m, 'time', Type::Time
@@ -244,6 +244,7 @@ def load_additional_types(type_map, oid = nil) # :nodoc:
244244
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
245245
ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql)
246246
ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql)
247+
ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgresql)
247248
ActiveRecord::Type.register(:json, Type::Json, adapter: :postgresql)
248249
ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql)
249250
ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql)

src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ protected void setObjectParameter(final ThreadContext context,
436436
break;
437437

438438
case "interval":
439-
statement.setObject(index, new PGInterval(value.toString()));
439+
statement.setObject(index, stringToPGInterval(value.toString()));
440440
break;
441441

442442
case "json":
@@ -493,6 +493,74 @@ protected void setObjectParameter(final ThreadContext context,
493493
}
494494
}
495495

496+
private int lookAhead(String value, int position, String find) {
497+
char [] tokens = find.toCharArray();
498+
int found = -1;
499+
500+
for ( int i = 0; i < tokens.length; i++ ) {
501+
found = value.indexOf(tokens[i], position);
502+
if ( found > 0 ) {
503+
return found;
504+
}
505+
}
506+
return found;
507+
}
508+
509+
private Object stringToPGInterval(String value) throws SQLException {
510+
if (!value.startsWith("P")) return new PGInterval(value);
511+
512+
PGInterval interval = new PGInterval();
513+
514+
/* this is copied from pgjdbc with fixes for Rails */
515+
int number = 0;
516+
String dateValue;
517+
String timeValue = null;
518+
519+
int hasTime = value.indexOf('T');
520+
if ( hasTime > 0 ) {
521+
/* skip over the P */
522+
dateValue = value.substring(1,hasTime);
523+
timeValue = value.substring(hasTime + 1);
524+
} else {
525+
/* skip over the P */
526+
dateValue = value.substring(1);
527+
}
528+
529+
for ( int i = 0; i < dateValue.length(); i++ ) {
530+
int lookAhead = lookAhead(dateValue, i, "YMD");
531+
if (lookAhead > 0) {
532+
char type = dateValue.charAt(lookAhead);
533+
number = Integer.parseInt(dateValue.substring(i, lookAhead));
534+
if (type == 'Y') {
535+
interval.setYears(number);
536+
} else if (type == 'M') {
537+
interval.setMonths(number);
538+
} else if (type == 'D') {
539+
interval.setDays(number);
540+
}
541+
i = lookAhead;
542+
}
543+
}
544+
if ( timeValue != null ) {
545+
for (int i = 0; i < timeValue.length(); i++) {
546+
int lookAhead = lookAhead(timeValue, i, "HMS");
547+
if (lookAhead > 0) {
548+
char type = timeValue.charAt(lookAhead);
549+
String part = timeValue.substring(i, lookAhead);
550+
if (timeValue.charAt(lookAhead) == 'H') {
551+
interval.setHours(Integer.parseInt(part));
552+
} else if (timeValue.charAt(lookAhead) == 'M') {
553+
interval.setMinutes(Integer.parseInt(part));
554+
} else if (timeValue.charAt(lookAhead) == 'S') {
555+
interval.setSeconds(Double.parseDouble(part));
556+
}
557+
i = lookAhead;
558+
}
559+
}
560+
}
561+
return interval;
562+
}
563+
496564
protected IRubyObject jdbcToRuby(ThreadContext context, Ruby runtime, int column, int type, ResultSet resultSet) throws SQLException {
497565
return typeMap != null ?
498566
convertWithTypeMap(context, runtime, column, type, resultSet) :
@@ -874,34 +942,25 @@ private IRubyObject parseInfinity(final Ruby runtime, final String value) {
874942
private static String formatInterval(final Object object) {
875943
final PGInterval interval = (PGInterval) object;
876944
final StringBuilder str = new StringBuilder(32);
945+
str.append("P");
877946

878947
final int years = interval.getYears();
879-
if (years != 0) str.append(years).append(" years ");
948+
if (years != 0) str.append(years).append("Y");
880949

881950
final int months = interval.getMonths();
882-
if (months != 0) str.append(months).append(" months ");
951+
if (months != 0) str.append(months).append("M");
883952

884953
final int days = interval.getDays();
885-
if (days != 0) str.append(days).append(" days ");
954+
if (days != 0) str.append(days).append("D");
886955

887956
final int hours = interval.getHours();
888957
final int mins = interval.getMinutes();
889-
final int secs = (int) interval.getSeconds();
890-
if (hours != 0 || mins != 0 || secs != 0) { // xx:yy:zz if not all 00
891-
if (hours < 10) str.append('0');
892-
893-
str.append(hours).append(':');
894-
895-
if (mins < 10) str.append('0');
896-
897-
str.append(mins).append(':');
898-
899-
if (secs < 10) str.append('0');
900-
901-
str.append(secs);
902-
903-
} else if (str.length() > 1) {
904-
str.deleteCharAt(str.length() - 1); // " " at the end
958+
final double secs = interval.getSeconds();
959+
if (hours != 0 || mins != 0 || secs != 0) {
960+
str.append("T");
961+
if (hours != 0) str.append(hours).append("H");
962+
if (mins != 0) str.append(mins).append("M");
963+
if (secs != 0) str.append(secs).append("S");
905964
}
906965

907966
return str.toString();

test/db/postgresql/types_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,8 @@ def test_data_type_of_number_types
300300
end
301301

302302
def test_data_type_of_time_types
303-
assert_instance_of OID::SpecializedString, PostgresqlTime.type_for_attribute('time_interval')
304-
assert_instance_of OID::SpecializedString, PostgresqlTime.type_for_attribute('scaled_time_interval')
303+
assert_instance_of OID::Interval, PostgresqlTime.type_for_attribute('time_interval')
304+
assert_instance_of OID::Interval, PostgresqlTime.type_for_attribute('scaled_time_interval')
305305
end
306306

307307
def test_data_type_of_network_address_types
@@ -554,8 +554,8 @@ def test_number_values
554554

555555
def test_time_values
556556
# omit_unless ar_version('4.0')
557-
assert_equal '-1 years -2 days', @first_time.time_interval
558-
assert_equal '-21 days', @first_time.scaled_time_interval
557+
assert_equal '-1 years and -2 days', @first_time.time_interval.inspect
558+
assert_equal '-21 days', @first_time.scaled_time_interval.inspect
559559
end
560560

561561
def test_network_address_values_ipaddr
@@ -644,10 +644,10 @@ def test_update_number
644644
end
645645

646646
def test_update_time
647-
@first_time.time_interval = '2 years 3 minutes'
647+
@first_time.time_interval = 'P2YT3M'
648648
@first_time.save!
649649
@first_time.reload
650-
assert_equal '2 years 00:03:00', @first_time.time_interval
650+
assert_equal '2 years and 3 minutes', @first_time.time_interval.inspect
651651
end
652652

653653
def test_update_network_address
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exclude :test_interval_type, 'Cannot auto-cast string to :interval using bind params'

0 commit comments

Comments
 (0)