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:
Yves Senn 2014-01-21 12:14:11 +01:00
parent 40a9d89877
commit 4cb47167e7
4 changed files with 81 additions and 52 deletions

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