Skip to content

Commit

Permalink
Merge pull request #7345 from slbug/master
Browse files Browse the repository at this point in the history
Postgresql range support
  • Loading branch information
rafaelfranca committed Jan 23, 2013
2 parents 1b75b94 + af1ef85 commit 3af85ed
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 189 deletions.
19 changes: 19 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,5 +1,24 @@
## Rails 4.0.0 (unreleased) ##

* PostgreSQL ranges type support. Includes: int4range, int8range,
numrange, tsrange, tstzrange, daterange

Ranges can be created with inclusive and exclusive bounds.

Example:

create_table :Room do |t|
t.daterange :availability
end

Room.create(availability: (Date.today..Float::INFINITY))
Room.first.availability # => Wed, 19 Sep 2012..Infinity

One thing to note: Range class does not support exclusive lower
bound.

*Alexander Grebennik*

* Added a state instance variable to each transaction. Will allow other objects
to know whether a transaction has been committed or rolled back.

Expand Down
Expand Up @@ -47,6 +47,9 @@ def default_string(value)
value.to_s
when Date, DateTime, Time
"'#{value.to_s(:db)}'"
when Range
# infinity dumps as Infinity, which causes uninitialized constant error
value.inspect.gsub('Infinity', '::Float::INFINITY')
else
value.inspect
end
Expand Down
Expand Up @@ -126,7 +126,6 @@ def type_cast_code(var_name)
when :hstore then "#{klass}.string_to_hstore(#{var_name})"
when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
when :json then "#{klass}.string_to_json(#{var_name})"
when :intrange then "#{klass}.string_to_intrange(#{var_name})"
else var_name
end
end
Expand Down
Expand Up @@ -62,6 +62,12 @@ def array_to_string(value, column, adapter, should_be_quoted = false)
"{#{casted_values.join(',')}}"
end

def range_to_string(object)
from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
"[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
end

def string_to_json(string)
if String === string
ActiveSupport::JSON.decode(string)
Expand Down Expand Up @@ -92,36 +98,6 @@ def string_to_array(string, oid)
parse_pg_array(string).map{|val| oid.type_cast val}
end

def string_to_intrange(string)
if string.nil?
nil
elsif "empty" == string
(nil..nil)
elsif String === string && (matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string))
lower_bound = ("(" == matches[1] ? (matches[2].to_i + 1) : matches[2].to_i)
upper_bound = (")" == matches[5] ? (matches[4].to_i - 1) : matches[4].to_i)
(lower_bound..upper_bound)
else
string
end
end

def intrange_to_string(object)
if object.nil?
nil
elsif Range === object
if [object.first, object.last].all? { |el| Integer === el }
"[#{object.first.to_i},#{object.exclude_end? ? object.last.to_i : object.last.to_i + 1})"
elsif [object.first, object.last].all? { |el| NilClass === el }
"empty"
else
nil
end
else
object
end
end

private

HstorePair = begin
Expand Down
Expand Up @@ -78,6 +78,64 @@ def type_cast(value)
end
end

class Range < Type
attr_reader :subtype
def initialize(subtype)
@subtype = subtype
end

def exctract_bounds(value)
from, to = value[1..-2].split(',')
{
from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
to: (value[-2] == ',' || to == 'infinity') ? 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
end

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

extracted = exctract_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

::Range.new(from, to, extracted[:exclude_end])
end
end

class Integer < Type
def type_cast(value)
return if value.nil?
Expand Down Expand Up @@ -168,14 +226,6 @@ def type_cast(value)
end
end

class IntRange < Type
def type_cast(value)
return if value.nil?

ConnectionAdapters::PostgreSQLColumn.string_to_intrange value
end
end

class TypeMap
def initialize
@mapping = {}
Expand Down Expand Up @@ -241,6 +291,13 @@ 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'
Expand Down Expand Up @@ -278,9 +335,6 @@ def self.registered_type?(name)
register_type 'json', OID::Json.new
register_type 'ltree', OID::Identity.new

register_type 'int4range', OID::IntRange.new
alias_type 'int8range', 'int4range'

register_type 'cidr', OID::Cidr.new
alias_type 'inet', 'cidr'
end
Expand Down
Expand Up @@ -19,6 +19,12 @@ def quote(value, column = nil) #:nodoc:
return super unless column

case value
when Range
if /range$/ =~ column.sql_type
"'#{PostgreSQLColumn.range_to_string(value)}'::#{column.sql_type}"
else
super
end
when Array
if column.array
"'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
Expand All @@ -31,11 +37,6 @@ def quote(value, column = nil) #:nodoc:
when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
else super
end
when Range
case column.sql_type
when 'int4range', 'int8range' then super(PostgreSQLColumn.intrange_to_string(value), column)
else super
end
when IPAddr
case column.sql_type
when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
Expand Down Expand Up @@ -74,6 +75,9 @@ def type_cast(value, column, array_member = false)
return super(value, column) unless column

case value
when Range
return super(value, column) unless /range$/ =~ column.sql_type
PostgreSQLColumn.range_to_string(value)
when NilClass
if column.array && array_member
'NULL'
Expand All @@ -94,11 +98,6 @@ def type_cast(value, column, array_member = false)
when 'json' then PostgreSQLColumn.json_to_string(value)
else super(value, column)
end
when Range
case column.sql_type
when 'int4range', 'int8range' then PostgreSQLColumn.intrange_to_string(value)
else super(value, column)
end
when IPAddr
return super(value, column) unless ['inet','cidr'].include? column.sql_type
PostgreSQLColumn.cidr_to_string(value)
Expand Down
Expand Up @@ -417,14 +417,6 @@ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
when 0..6; "timestamp(#{precision})"
else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
end
when 'intrange'
return 'int4range' unless limit

case limit
when 1..4; 'int4range'
when 5..8; 'int8range'
else raise(ActiveRecordError, "No range type has byte size #{limit}. Use a numeric with precision 0 instead.")
end
else
super
end
Expand Down
Expand Up @@ -77,6 +77,8 @@ def self.extract_value_from_default(default)
return default unless default

case default
when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
$1
# Numeric types
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
$1
Expand Down Expand Up @@ -117,9 +119,6 @@ def self.extract_value_from_default(default)
# JSON
when /\A'(.*)'::json\z/
$1
# int4range, int8range
when /\A'(.*)'::int(4|8)range\z/
$1
# Object identifier types
when /\A-?\d+\z/
$1
Expand Down Expand Up @@ -220,12 +219,11 @@ def simplified_type(field_type)
# JSON type
when 'json'
:json
# int4range, int8range types
when 'int4range', 'int8range'
:intrange
# Small and big integer types
when /^(?:small|big)int$/
:integer
when /(num|date|tstz|ts|int4|int8)range$/
field_type.to_sym
# Pass through all types that are not specific to PostgreSQL.
else
super
Expand Down Expand Up @@ -276,6 +274,30 @@ def tsvector(*args)
column(args[0], 'tsvector', options)
end

def int4range(name, options = {})
column(name, 'int4range', options)
end

def int8range(name, options = {})
column(name, 'int8range', options)
end

def tsrange(name, options = {})
column(name, 'tsrange', options)
end

def tstzrange(name, options = {})
column(name, 'tstzrange', options)
end

def numrange(name, options = {})
column(name, 'numrange', options)
end

def daterange(name, options = {})
column(name, 'daterange', options)
end

def hstore(name, options = {})
column(name, 'hstore', options)
end
Expand Down Expand Up @@ -304,10 +326,6 @@ def json(name, options = {})
column(name, 'json', options)
end

def intrange(name, options = {})
column(name, 'intrange', options)
end

def column(name, type = nil, options = {})
super
column = self[name]
Expand Down Expand Up @@ -339,6 +357,12 @@ def new_column_definition(base, name, type)
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
daterange: { name: "daterange" },
numrange: { name: "numrange" },
tsrange: { name: "tsrange" },
tstzrange: { name: "tstzrange" },
int4range: { name: "int4range" },
int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
xml: { name: "xml" },
Expand All @@ -349,7 +373,6 @@ def new_column_definition(base, name, type)
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
intrange: { name: "int4range" },
ltree: { name: "ltree" }
}

Expand Down

0 comments on commit 3af85ed

Please sign in to comment.