Skip to content
Permalink
Browse files

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.
  • Loading branch information
senny committed Feb 23, 2014
1 parent 40a9d89 commit 4cb47167e747e8f9dc12b0ddaf82bdb68c03e032
@@ -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,51 +113,28 @@ 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)
return if value.nil? || value == 'empty'
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,39 +49,44 @@ 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'',]",
num_range: "[0.1,]",
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: "[,]",
num_range: "[,]",
ts_range: "[,]",
tstz_range: "[,]",
int4_range: "[,]",
int8_range: "[,]")
int8_range: "[,]",
float_range: "[,]")

insert_range(id: 105,
date_range: "(''2012-01-02'', ''2012-01-02'')",
num_range: "(0.1, 0.1)",
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,15 +256,17 @@ def insert_range(values)
ts_range,
tstz_range,
int4_range,
int8_range
int8_range,
float_range
) VALUES (
#{values[:id]},
'#{values[:date_range]}',
'#{values[:num_range]}',
'#{values[:ts_range]}',
'#{values[:tstz_range]}',
'#{values[:int4_range]}',
'#{values[:int8_range]}'
'#{values[:int8_range]}',
'#{values[:float_range]}'
)
SQL
end

0 comments on commit 4cb4716

Please sign in to comment.
You can’t perform that action at this time.