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.
This commit is contained in:
parent
40a9d89877
commit
4cb47167e7
@ -1,3 +1,7 @@
|
||||
* Support for user created range types in PostgreSQL.
|
||||
|
||||
*Yves Senn*
|
||||
|
||||
* Default scopes are no longer overriden by chained conditions.
|
||||
|
||||
Before this change when you defined a `default_scope` in a model
|
||||
|
@ -6,6 +6,10 @@ class PostgreSQLAdapter < AbstractAdapter
|
||||
module OID
|
||||
class Type
|
||||
def type; end
|
||||
|
||||
def infinity(options = {})
|
||||
::Float::INFINITY * (options[:negative] ? -1 : 1)
|
||||
end
|
||||
end
|
||||
|
||||
class Identity < Type
|
||||
@ -109,23 +113,19 @@ def initialize(subtype)
|
||||
def extract_bounds(value)
|
||||
from, to = value[1..-2].split(',')
|
||||
{
|
||||
from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
|
||||
to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
|
||||
from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
|
||||
to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
|
||||
exclude_start: (value[0] == '('),
|
||||
exclude_end: (value[-1] == ')')
|
||||
}
|
||||
end
|
||||
|
||||
def infinity(options = {})
|
||||
::Float::INFINITY * (options[:negative] ? -1 : 1)
|
||||
end
|
||||
|
||||
def infinity?(value)
|
||||
value.respond_to?(:infinite?) && value.infinite?
|
||||
end
|
||||
|
||||
def to_integer(value)
|
||||
infinity?(value) ? value : value.to_i
|
||||
def type_cast_single(value)
|
||||
infinity?(value) ? value : @subtype.type_cast(value)
|
||||
end
|
||||
|
||||
def type_cast(value)
|
||||
@ -133,27 +133,8 @@ def type_cast(value)
|
||||
return value if value.is_a?(::Range)
|
||||
|
||||
extracted = extract_bounds(value)
|
||||
|
||||
case @subtype
|
||||
when :date
|
||||
from = ConnectionAdapters::Column.value_to_date(extracted[:from])
|
||||
from -= 1.day if extracted[:exclude_start]
|
||||
to = ConnectionAdapters::Column.value_to_date(extracted[:to])
|
||||
when :decimal
|
||||
from = BigDecimal.new(extracted[:from].to_s)
|
||||
# FIXME: add exclude start for ::Range, same for timestamp ranges
|
||||
to = BigDecimal.new(extracted[:to].to_s)
|
||||
when :time
|
||||
from = ConnectionAdapters::Column.string_to_time(extracted[:from])
|
||||
to = ConnectionAdapters::Column.string_to_time(extracted[:to])
|
||||
when :integer
|
||||
from = to_integer(extracted[:from]) rescue value ? 1 : 0
|
||||
from -= 1 if extracted[:exclude_start]
|
||||
to = to_integer(extracted[:to]) rescue value ? 1 : 0
|
||||
else
|
||||
return value
|
||||
end
|
||||
|
||||
from = type_cast_single extracted[:from]
|
||||
to = type_cast_single extracted[:to]
|
||||
::Range.new(from, to, extracted[:exclude_end])
|
||||
end
|
||||
end
|
||||
@ -222,6 +203,10 @@ def type_cast(value)
|
||||
|
||||
ConnectionAdapters::Column.value_to_decimal value
|
||||
end
|
||||
|
||||
def infinity(options = {})
|
||||
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
|
||||
end
|
||||
end
|
||||
|
||||
class Hstore < Type
|
||||
@ -331,13 +316,6 @@ def self.registered_type?(name)
|
||||
alias_type 'int8', 'int2'
|
||||
alias_type 'oid', 'int2'
|
||||
|
||||
register_type 'daterange', OID::Range.new(:date)
|
||||
register_type 'numrange', OID::Range.new(:decimal)
|
||||
register_type 'tsrange', OID::Range.new(:time)
|
||||
register_type 'int4range', OID::Range.new(:integer)
|
||||
alias_type 'tstzrange', 'tsrange'
|
||||
alias_type 'int8range', 'int4range'
|
||||
|
||||
register_type 'numeric', OID::Decimal.new
|
||||
register_type 'text', OID::Identity.new
|
||||
alias_type 'varchar', 'text'
|
||||
|
@ -785,18 +785,29 @@ def add_oid(row, records_by_oid, type_map)
|
||||
end
|
||||
|
||||
def initialize_type_map(type_map)
|
||||
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
|
||||
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
|
||||
if supports_ranges?
|
||||
result = execute(<<-SQL, 'SCHEMA')
|
||||
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype
|
||||
FROM pg_type as t
|
||||
LEFT JOIN pg_range as r ON oid = rngtypid
|
||||
SQL
|
||||
else
|
||||
result = execute(<<-SQL, 'SCHEMA')
|
||||
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput
|
||||
FROM pg_type as t
|
||||
SQL
|
||||
end
|
||||
ranges, nodes = result.partition { |row| row['typinput'] == 'range_in' }
|
||||
leaves, nodes = nodes.partition { |row| row['typelem'] == '0' }
|
||||
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
|
||||
|
||||
# populate the leaf nodes
|
||||
# populate the base types
|
||||
leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
|
||||
type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
|
||||
end
|
||||
|
||||
records_by_oid = result.group_by { |row| row['oid'] }
|
||||
|
||||
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
|
||||
|
||||
# populate composite types
|
||||
nodes.each do |row|
|
||||
add_oid row, records_by_oid, type_map
|
||||
@ -807,6 +818,13 @@ def initialize_type_map(type_map)
|
||||
array = OID::Array.new type_map[row['typelem'].to_i]
|
||||
type_map[row['oid'].to_i] = array
|
||||
end
|
||||
|
||||
# populate range types
|
||||
ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row|
|
||||
subtype = type_map[row['rngsubtype'].to_i]
|
||||
range = OID::Range.new type_map[row['rngsubtype'].to_i]
|
||||
type_map[row['oid'].to_i] = range
|
||||
end
|
||||
end
|
||||
|
||||
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
|
||||
|
@ -10,12 +10,22 @@ class PostgresqlRange < ActiveRecord::Base
|
||||
class PostgresqlRangeTest < ActiveRecord::TestCase
|
||||
def teardown
|
||||
@connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
|
||||
@connection.execute 'DROP TYPE IF EXISTS floatrange'
|
||||
end
|
||||
|
||||
def setup
|
||||
@connection = ActiveRecord::Base.connection
|
||||
@connection = PostgresqlRange.connection
|
||||
begin
|
||||
@connection.transaction do
|
||||
@connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
|
||||
@connection.execute 'DROP TYPE IF EXISTS floatrange'
|
||||
@connection.execute <<_SQL
|
||||
CREATE TYPE floatrange AS RANGE (
|
||||
subtype = float8,
|
||||
subtype_diff = float8mi
|
||||
);
|
||||
_SQL
|
||||
|
||||
@connection.create_table('postgresql_ranges') do |t|
|
||||
t.daterange :date_range
|
||||
t.numrange :num_range
|
||||
@ -24,7 +34,11 @@ def setup
|
||||
t.int4range :int4_range
|
||||
t.int8range :int8_range
|
||||
end
|
||||
|
||||
@connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
|
||||
end
|
||||
@connection.send :reload_type_map
|
||||
PostgresqlRange.reset_column_information
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
skip "do not test on PG without range"
|
||||
end
|
||||
@ -35,15 +49,17 @@ def setup
|
||||
ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
|
||||
tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
|
||||
int4_range: "[1, 10]",
|
||||
int8_range: "[10, 100]")
|
||||
int8_range: "[10, 100]",
|
||||
float_range: "[0.5, 0.7]")
|
||||
|
||||
insert_range(id: 102,
|
||||
date_range: "(''2012-01-02'', ''2012-01-04'')",
|
||||
num_range: "[0.1, 0.2)",
|
||||
ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
|
||||
tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
|
||||
num_range: "(0.1, 0.2)",
|
||||
ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'')",
|
||||
tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
|
||||
int4_range: "(1, 10)",
|
||||
int8_range: "(10, 100)")
|
||||
int8_range: "(10, 100)",
|
||||
float_range: "(0.5, 0.7)")
|
||||
|
||||
insert_range(id: 103,
|
||||
date_range: "(''2012-01-02'',]",
|
||||
@ -51,7 +67,8 @@ def setup
|
||||
ts_range: "[''2010-01-01 14:30'',]",
|
||||
tstz_range: "[''2010-01-01 14:30:00+05'',]",
|
||||
int4_range: "(1,]",
|
||||
int8_range: "(10,]")
|
||||
int8_range: "(10,]",
|
||||
float_range: "[0.5,]")
|
||||
|
||||
insert_range(id: 104,
|
||||
date_range: "[,]",
|
||||
@ -59,7 +76,8 @@ def setup
|
||||
ts_range: "[,]",
|
||||
tstz_range: "[,]",
|
||||
int4_range: "[,]",
|
||||
int8_range: "[,]")
|
||||
int8_range: "[,]",
|
||||
float_range: "[,]")
|
||||
|
||||
insert_range(id: 105,
|
||||
date_range: "(''2012-01-02'', ''2012-01-02'')",
|
||||
@ -67,7 +85,8 @@ def setup
|
||||
ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')",
|
||||
tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
|
||||
int4_range: "(1, 1)",
|
||||
int8_range: "(10, 10)")
|
||||
int8_range: "(10, 10)",
|
||||
float_range: "(0.5, 0.5)")
|
||||
|
||||
@new_range = PostgresqlRange.new
|
||||
@first_range = PostgresqlRange.find(101)
|
||||
@ -133,6 +152,14 @@ def test_tstzrange_values
|
||||
assert_nil @empty_range.tstz_range
|
||||
end
|
||||
|
||||
def test_custom_range_values
|
||||
assert_equal 0.5..0.7, @first_range.float_range
|
||||
assert_equal 0.5...0.7, @second_range.float_range
|
||||
assert_equal 0.5...Float::INFINITY, @third_range.float_range
|
||||
assert_equal -Float::INFINITY...Float::INFINITY, @fourth_range.float_range
|
||||
assert_nil @empty_range.float_range
|
||||
end
|
||||
|
||||
def test_create_tstzrange
|
||||
tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
|
||||
round_trip(@new_range, :tstz_range, tstzrange)
|
||||
@ -229,7 +256,8 @@ def insert_range(values)
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
int8_range,
|
||||
float_range
|
||||
) VALUES (
|
||||
#{values[:id]},
|
||||
'#{values[:date_range]}',
|
||||
@ -237,7 +265,8 @@ def insert_range(values)
|
||||
'#{values[:ts_range]}',
|
||||
'#{values[:tstz_range]}',
|
||||
'#{values[:int4_range]}',
|
||||
'#{values[:int8_range]}'
|
||||
'#{values[:int8_range]}',
|
||||
'#{values[:float_range]}'
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user