Skip to content

Commit 4cb4716

Browse files
committed
dynamically define PostgreSQL OID range types.
This gets AR working with custom defined range types. It also removes the need for subtype specific branches in `OID::Range`. This expands the interface of all `OID` types with the `infinity` method. It's responsible to provide a value for positive and negative infinity.
1 parent 40a9d89 commit 4cb4716

4 files changed

Lines changed: 81 additions & 52 deletions

File tree

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Support for user created range types in PostgreSQL.
2+
3+
*Yves Senn*
4+
15
* Default scopes are no longer overriden by chained conditions.
26

37
Before this change when you defined a `default_scope` in a model

activerecord/lib/active_record/connection_adapters/postgresql/oid.rb

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ class PostgreSQLAdapter < AbstractAdapter
66
module OID
77
class Type
88
def type; end
9+
10+
def infinity(options = {})
11+
::Float::INFINITY * (options[:negative] ? -1 : 1)
12+
end
913
end
1014

1115
class Identity < Type
@@ -109,51 +113,28 @@ def initialize(subtype)
109113
def extract_bounds(value)
110114
from, to = value[1..-2].split(',')
111115
{
112-
from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
113-
to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
116+
from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
117+
to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
114118
exclude_start: (value[0] == '('),
115119
exclude_end: (value[-1] == ')')
116120
}
117121
end
118122

119-
def infinity(options = {})
120-
::Float::INFINITY * (options[:negative] ? -1 : 1)
121-
end
122-
123123
def infinity?(value)
124124
value.respond_to?(:infinite?) && value.infinite?
125125
end
126126

127-
def to_integer(value)
128-
infinity?(value) ? value : value.to_i
127+
def type_cast_single(value)
128+
infinity?(value) ? value : @subtype.type_cast(value)
129129
end
130130

131131
def type_cast(value)
132132
return if value.nil? || value == 'empty'
133133
return value if value.is_a?(::Range)
134134

135135
extracted = extract_bounds(value)
136-
137-
case @subtype
138-
when :date
139-
from = ConnectionAdapters::Column.value_to_date(extracted[:from])
140-
from -= 1.day if extracted[:exclude_start]
141-
to = ConnectionAdapters::Column.value_to_date(extracted[:to])
142-
when :decimal
143-
from = BigDecimal.new(extracted[:from].to_s)
144-
# FIXME: add exclude start for ::Range, same for timestamp ranges
145-
to = BigDecimal.new(extracted[:to].to_s)
146-
when :time
147-
from = ConnectionAdapters::Column.string_to_time(extracted[:from])
148-
to = ConnectionAdapters::Column.string_to_time(extracted[:to])
149-
when :integer
150-
from = to_integer(extracted[:from]) rescue value ? 1 : 0
151-
from -= 1 if extracted[:exclude_start]
152-
to = to_integer(extracted[:to]) rescue value ? 1 : 0
153-
else
154-
return value
155-
end
156-
136+
from = type_cast_single extracted[:from]
137+
to = type_cast_single extracted[:to]
157138
::Range.new(from, to, extracted[:exclude_end])
158139
end
159140
end
@@ -222,6 +203,10 @@ def type_cast(value)
222203

223204
ConnectionAdapters::Column.value_to_decimal value
224205
end
206+
207+
def infinity(options = {})
208+
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
209+
end
225210
end
226211

227212
class Hstore < Type
@@ -331,13 +316,6 @@ def self.registered_type?(name)
331316
alias_type 'int8', 'int2'
332317
alias_type 'oid', 'int2'
333318

334-
register_type 'daterange', OID::Range.new(:date)
335-
register_type 'numrange', OID::Range.new(:decimal)
336-
register_type 'tsrange', OID::Range.new(:time)
337-
register_type 'int4range', OID::Range.new(:integer)
338-
alias_type 'tstzrange', 'tsrange'
339-
alias_type 'int8range', 'int4range'
340-
341319
register_type 'numeric', OID::Decimal.new
342320
register_type 'text', OID::Identity.new
343321
alias_type 'varchar', 'text'

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -785,18 +785,29 @@ def add_oid(row, records_by_oid, type_map)
785785
end
786786

787787
def initialize_type_map(type_map)
788-
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
789-
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
788+
if supports_ranges?
789+
result = execute(<<-SQL, 'SCHEMA')
790+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype
791+
FROM pg_type as t
792+
LEFT JOIN pg_range as r ON oid = rngtypid
793+
SQL
794+
else
795+
result = execute(<<-SQL, 'SCHEMA')
796+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput
797+
FROM pg_type as t
798+
SQL
799+
end
800+
ranges, nodes = result.partition { |row| row['typinput'] == 'range_in' }
801+
leaves, nodes = nodes.partition { |row| row['typelem'] == '0' }
802+
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
790803

791-
# populate the leaf nodes
804+
# populate the base types
792805
leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
793806
type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
794807
end
795808

796809
records_by_oid = result.group_by { |row| row['oid'] }
797810

798-
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
799-
800811
# populate composite types
801812
nodes.each do |row|
802813
add_oid row, records_by_oid, type_map
@@ -807,6 +818,13 @@ def initialize_type_map(type_map)
807818
array = OID::Array.new type_map[row['typelem'].to_i]
808819
type_map[row['oid'].to_i] = array
809820
end
821+
822+
# populate range types
823+
ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row|
824+
subtype = type_map[row['rngsubtype'].to_i]
825+
range = OID::Range.new type_map[row['rngsubtype'].to_i]
826+
type_map[row['oid'].to_i] = range
827+
end
810828
end
811829

812830
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:

activerecord/test/cases/adapters/postgresql/range_test.rb

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ class PostgresqlRange < ActiveRecord::Base
1010
class PostgresqlRangeTest < ActiveRecord::TestCase
1111
def teardown
1212
@connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
13+
@connection.execute 'DROP TYPE IF EXISTS floatrange'
1314
end
1415

1516
def setup
16-
@connection = ActiveRecord::Base.connection
17+
@connection = PostgresqlRange.connection
1718
begin
1819
@connection.transaction do
20+
@connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
21+
@connection.execute 'DROP TYPE IF EXISTS floatrange'
22+
@connection.execute <<_SQL
23+
CREATE TYPE floatrange AS RANGE (
24+
subtype = float8,
25+
subtype_diff = float8mi
26+
);
27+
_SQL
28+
1929
@connection.create_table('postgresql_ranges') do |t|
2030
t.daterange :date_range
2131
t.numrange :num_range
@@ -24,7 +34,11 @@ def setup
2434
t.int4range :int4_range
2535
t.int8range :int8_range
2636
end
37+
38+
@connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
2739
end
40+
@connection.send :reload_type_map
41+
PostgresqlRange.reset_column_information
2842
rescue ActiveRecord::StatementInvalid
2943
skip "do not test on PG without range"
3044
end
@@ -35,39 +49,44 @@ def setup
3549
ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
3650
tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
3751
int4_range: "[1, 10]",
38-
int8_range: "[10, 100]")
52+
int8_range: "[10, 100]",
53+
float_range: "[0.5, 0.7]")
3954

4055
insert_range(id: 102,
4156
date_range: "(''2012-01-02'', ''2012-01-04'')",
42-
num_range: "[0.1, 0.2)",
43-
ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
44-
tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
57+
num_range: "(0.1, 0.2)",
58+
ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'')",
59+
tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
4560
int4_range: "(1, 10)",
46-
int8_range: "(10, 100)")
61+
int8_range: "(10, 100)",
62+
float_range: "(0.5, 0.7)")
4763

4864
insert_range(id: 103,
4965
date_range: "(''2012-01-02'',]",
5066
num_range: "[0.1,]",
5167
ts_range: "[''2010-01-01 14:30'',]",
5268
tstz_range: "[''2010-01-01 14:30:00+05'',]",
5369
int4_range: "(1,]",
54-
int8_range: "(10,]")
70+
int8_range: "(10,]",
71+
float_range: "[0.5,]")
5572

5673
insert_range(id: 104,
5774
date_range: "[,]",
5875
num_range: "[,]",
5976
ts_range: "[,]",
6077
tstz_range: "[,]",
6178
int4_range: "[,]",
62-
int8_range: "[,]")
79+
int8_range: "[,]",
80+
float_range: "[,]")
6381

6482
insert_range(id: 105,
6583
date_range: "(''2012-01-02'', ''2012-01-02'')",
6684
num_range: "(0.1, 0.1)",
6785
ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')",
6886
tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
6987
int4_range: "(1, 1)",
70-
int8_range: "(10, 10)")
88+
int8_range: "(10, 10)",
89+
float_range: "(0.5, 0.5)")
7190

7291
@new_range = PostgresqlRange.new
7392
@first_range = PostgresqlRange.find(101)
@@ -133,6 +152,14 @@ def test_tstzrange_values
133152
assert_nil @empty_range.tstz_range
134153
end
135154

155+
def test_custom_range_values
156+
assert_equal 0.5..0.7, @first_range.float_range
157+
assert_equal 0.5...0.7, @second_range.float_range
158+
assert_equal 0.5...Float::INFINITY, @third_range.float_range
159+
assert_equal -Float::INFINITY...Float::INFINITY, @fourth_range.float_range
160+
assert_nil @empty_range.float_range
161+
end
162+
136163
def test_create_tstzrange
137164
tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
138165
round_trip(@new_range, :tstz_range, tstzrange)
@@ -229,15 +256,17 @@ def insert_range(values)
229256
ts_range,
230257
tstz_range,
231258
int4_range,
232-
int8_range
259+
int8_range,
260+
float_range
233261
) VALUES (
234262
#{values[:id]},
235263
'#{values[:date_range]}',
236264
'#{values[:num_range]}',
237265
'#{values[:ts_range]}',
238266
'#{values[:tstz_range]}',
239267
'#{values[:int4_range]}',
240-
'#{values[:int8_range]}'
268+
'#{values[:int8_range]}',
269+
'#{values[:float_range]}'
241270
)
242271
SQL
243272
end

0 commit comments

Comments
 (0)