mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add postgresql range types support
This commit is contained in:
parent
e20c0e3a8f
commit
af1ef85ad2
11 changed files with 431 additions and 189 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -47,6 +47,9 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -126,7 +126,6 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -62,6 +62,12 @@ module ActiveRecord
|
|||
"{#{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)
|
||||
|
@ -92,36 +98,6 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -78,6 +78,64 @@ module ActiveRecord
|
|||
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?
|
||||
|
@ -168,14 +226,6 @@ module ActiveRecord
|
|||
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 = {}
|
||||
|
@ -241,6 +291,13 @@ module ActiveRecord
|
|||
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'
|
||||
|
@ -278,9 +335,6 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -19,6 +19,12 @@ module ActiveRecord
|
|||
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)}'"
|
||||
|
@ -31,11 +37,6 @@ module ActiveRecord
|
|||
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)
|
||||
|
@ -74,6 +75,9 @@ module ActiveRecord
|
|||
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'
|
||||
|
@ -94,11 +98,6 @@ module ActiveRecord
|
|||
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)
|
||||
|
|
|
@ -417,14 +417,6 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -77,6 +77,8 @@ module ActiveRecord
|
|||
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
|
||||
|
@ -117,9 +119,6 @@ module ActiveRecord
|
|||
# JSON
|
||||
when /\A'(.*)'::json\z/
|
||||
$1
|
||||
# int4range, int8range
|
||||
when /\A'(.*)'::int(4|8)range\z/
|
||||
$1
|
||||
# Object identifier types
|
||||
when /\A-?\d+\z/
|
||||
$1
|
||||
|
@ -220,12 +219,11 @@ module ActiveRecord
|
|||
# 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
|
||||
|
@ -276,6 +274,30 @@ module ActiveRecord
|
|||
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
|
||||
|
@ -304,10 +326,6 @@ module ActiveRecord
|
|||
column(name, 'json', options)
|
||||
end
|
||||
|
||||
def intrange(name, options = {})
|
||||
column(name, 'intrange', options)
|
||||
end
|
||||
|
||||
def column(name, type = nil, options = {})
|
||||
super
|
||||
column = self[name]
|
||||
|
@ -339,6 +357,12 @@ module ActiveRecord
|
|||
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" },
|
||||
|
@ -349,7 +373,6 @@ module ActiveRecord
|
|||
macaddr: { name: "macaddr" },
|
||||
uuid: { name: "uuid" },
|
||||
json: { name: "json" },
|
||||
intrange: { name: "int4range" },
|
||||
ltree: { name: "ltree" }
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ require "cases/helper"
|
|||
class PostgresqlArray < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class PostgresqlRange < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class PostgresqlTsvector < ActiveRecord::Base
|
||||
end
|
||||
|
||||
|
@ -43,7 +46,104 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
|
|||
@connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')")
|
||||
@first_array = PostgresqlArray.find(1)
|
||||
|
||||
@connection.execute <<_SQL
|
||||
INSERT INTO postgresql_ranges (
|
||||
date_range,
|
||||
num_range,
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
) VALUES (
|
||||
'[''2012-01-02'', ''2012-01-04'']',
|
||||
'[0.1, 0.2]',
|
||||
'[''2010-01-01 14:30'', ''2011-01-01 14:30'']',
|
||||
'[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']',
|
||||
'[1, 10]',
|
||||
'[10, 100]'
|
||||
)
|
||||
_SQL
|
||||
|
||||
@connection.execute <<_SQL
|
||||
INSERT INTO postgresql_ranges (
|
||||
date_range,
|
||||
num_range,
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
) VALUES (
|
||||
'(''2012-01-02'', ''2012-01-04'')',
|
||||
'[0.1, 0.2)',
|
||||
'[''2010-01-01 14:30'', ''2011-01-01 14:30'')',
|
||||
'[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')',
|
||||
'(1, 10)',
|
||||
'(10, 100)'
|
||||
)
|
||||
_SQL
|
||||
|
||||
@connection.execute <<_SQL
|
||||
INSERT INTO postgresql_ranges (
|
||||
date_range,
|
||||
num_range,
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
) VALUES (
|
||||
'(''2012-01-02'',]',
|
||||
'[0.1,]',
|
||||
'[''2010-01-01 14:30'',]',
|
||||
'[''2010-01-01 14:30:00+05'',]',
|
||||
'(1,]',
|
||||
'(10,]'
|
||||
)
|
||||
_SQL
|
||||
|
||||
@connection.execute <<_SQL
|
||||
INSERT INTO postgresql_ranges (
|
||||
date_range,
|
||||
num_range,
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
) VALUES (
|
||||
'[,]',
|
||||
'[,]',
|
||||
'[,]',
|
||||
'[,]',
|
||||
'[,]',
|
||||
'[,]'
|
||||
)
|
||||
_SQL
|
||||
|
||||
@connection.execute <<_SQL
|
||||
INSERT INTO postgresql_ranges (
|
||||
date_range,
|
||||
num_range,
|
||||
ts_range,
|
||||
tstz_range,
|
||||
int4_range,
|
||||
int8_range
|
||||
) VALUES (
|
||||
'(''2012-01-02'', ''2012-01-02'')',
|
||||
'(0.1, 0.1)',
|
||||
'(''2010-01-01 14:30'', ''2010-01-01 14:30'')',
|
||||
'(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')',
|
||||
'(1, 1)',
|
||||
'(10, 10)'
|
||||
)
|
||||
_SQL
|
||||
|
||||
@first_range = PostgresqlRange.find(1)
|
||||
@second_range = PostgresqlRange.find(2)
|
||||
@third_range = PostgresqlRange.find(3)
|
||||
@fourth_range = PostgresqlRange.find(4)
|
||||
@empty_range = PostgresqlRange.find(5)
|
||||
|
||||
@connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
|
||||
|
||||
@first_tsvector = PostgresqlTsvector.find(1)
|
||||
|
||||
@connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)")
|
||||
|
@ -82,6 +182,15 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
|
|||
assert_equal :text, @first_array.column_for_attribute(:nicknames).type
|
||||
end
|
||||
|
||||
def test_data_type_of_range_types
|
||||
assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
|
||||
assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
|
||||
assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
|
||||
assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
|
||||
assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
|
||||
assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
|
||||
end
|
||||
|
||||
def test_data_type_of_tsvector_types
|
||||
assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
|
||||
end
|
||||
|
@ -128,11 +237,183 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
|
|||
assert_equal "'text' 'vector'", @first_tsvector.text_vector
|
||||
end
|
||||
|
||||
def test_int4range_values
|
||||
assert_equal 1...11, @first_range.int4_range
|
||||
assert_equal 2...10, @second_range.int4_range
|
||||
assert_equal 2...Float::INFINITY, @third_range.int4_range
|
||||
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
|
||||
assert_equal nil, @empty_range.int4_range
|
||||
end
|
||||
|
||||
def test_int8range_values
|
||||
assert_equal 10...101, @first_range.int8_range
|
||||
assert_equal 11...100, @second_range.int8_range
|
||||
assert_equal 11...Float::INFINITY, @third_range.int8_range
|
||||
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
|
||||
assert_equal nil, @empty_range.int8_range
|
||||
end
|
||||
|
||||
def test_daterange_values
|
||||
assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
|
||||
assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range
|
||||
assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range
|
||||
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
|
||||
assert_equal nil, @empty_range.date_range
|
||||
end
|
||||
|
||||
def test_numrange_values
|
||||
assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
|
||||
assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
|
||||
assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
|
||||
assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
|
||||
assert_equal nil, @empty_range.num_range
|
||||
end
|
||||
|
||||
def test_tsrange_values
|
||||
tz = ::ActiveRecord::Base.default_timezone
|
||||
assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
|
||||
assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
|
||||
assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Float::INFINITY, @third_range.ts_range
|
||||
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
|
||||
assert_equal nil, @empty_range.ts_range
|
||||
end
|
||||
|
||||
def test_tstzrange_values
|
||||
assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
|
||||
assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
|
||||
assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Float::INFINITY, @third_range.tstz_range
|
||||
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
|
||||
assert_equal nil, @empty_range.tstz_range
|
||||
end
|
||||
|
||||
def test_money_values
|
||||
assert_equal 567.89, @first_money.wealth
|
||||
assert_equal(-567.89, @second_money.wealth)
|
||||
end
|
||||
|
||||
def test_create_tstzrange
|
||||
tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
|
||||
range = PostgresqlRange.new(:tstz_range => tstzrange)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.tstz_range, tstzrange
|
||||
assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
|
||||
end
|
||||
|
||||
def test_update_tstzrange
|
||||
new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')
|
||||
assert @first_range.tstz_range = new_tstzrange
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.tstz_range, new_tstzrange
|
||||
assert @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.tstz_range, nil
|
||||
end
|
||||
|
||||
def test_create_tsrange
|
||||
tz = ::ActiveRecord::Base.default_timezone
|
||||
tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
|
||||
range = PostgresqlRange.new(:ts_range => tsrange)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.ts_range, tsrange
|
||||
end
|
||||
|
||||
def test_update_tsrange
|
||||
tz = ::ActiveRecord::Base.default_timezone
|
||||
new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
|
||||
assert @first_range.ts_range = new_tsrange
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.ts_range, new_tsrange
|
||||
assert @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.ts_range, nil
|
||||
end
|
||||
|
||||
def test_create_numrange
|
||||
numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
|
||||
range = PostgresqlRange.new(:num_range => numrange)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.num_range, numrange
|
||||
end
|
||||
|
||||
def test_update_numrange
|
||||
new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
|
||||
assert @first_range.num_range = new_numrange
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.num_range, new_numrange
|
||||
assert @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5')
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.num_range, nil
|
||||
end
|
||||
|
||||
def test_create_daterange
|
||||
daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)
|
||||
range = PostgresqlRange.new(:date_range => daterange)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.date_range, daterange
|
||||
end
|
||||
|
||||
def test_update_daterange
|
||||
new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10)
|
||||
assert @first_range.date_range = new_daterange
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.date_range, new_daterange
|
||||
assert @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3)
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.date_range, nil
|
||||
end
|
||||
|
||||
def test_create_int4range
|
||||
int4range = Range.new(3, 50, true)
|
||||
range = PostgresqlRange.new(:int4_range => int4range)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.int4_range, int4range
|
||||
end
|
||||
|
||||
def test_update_int4range
|
||||
new_int4range = 6...10
|
||||
assert @first_range.int4_range = new_int4range
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.int4_range, new_int4range
|
||||
assert @first_range.int4_range = 3...3
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.int4_range, nil
|
||||
end
|
||||
|
||||
def test_create_int8range
|
||||
int8range = Range.new(30, 50, true)
|
||||
range = PostgresqlRange.new(:int8_range => int8range)
|
||||
assert range.save
|
||||
assert range.reload
|
||||
assert_equal range.int8_range, int8range
|
||||
end
|
||||
|
||||
def test_update_int8range
|
||||
new_int8range = 60000...10000000
|
||||
assert @first_range.int8_range = new_int8range
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.int8_range, new_int8range
|
||||
assert @first_range.int8_range = 39999...39999
|
||||
assert @first_range.save
|
||||
assert @first_range.reload
|
||||
assert_equal @first_range.int8_range, nil
|
||||
end
|
||||
|
||||
def test_update_tsvector
|
||||
new_text_vector = "'new' 'text' 'vector'"
|
||||
assert @first_tsvector.text_vector = new_text_vector
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
# encoding: utf-8
|
||||
|
||||
require "cases/helper"
|
||||
require 'active_record/base'
|
||||
require 'active_record/connection_adapters/postgresql_adapter'
|
||||
|
||||
class PostgresqlIntrangesTest < ActiveRecord::TestCase
|
||||
class IntRangeDataType < ActiveRecord::Base
|
||||
self.table_name = 'intrange_data_type'
|
||||
end
|
||||
|
||||
def setup
|
||||
@connection = ActiveRecord::Base.connection
|
||||
begin
|
||||
@connection.transaction do
|
||||
@connection.create_table('intrange_data_type') do |t|
|
||||
t.intrange 'int_range', :default => (1..10)
|
||||
t.intrange 'long_int_range', :limit => 8, :default => (1..100)
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
return skip "do not test on PG without ranges"
|
||||
end
|
||||
@int_range_column = IntRangeDataType.columns.find { |c| c.name == 'int_range' }
|
||||
@long_int_range_column = IntRangeDataType.columns.find { |c| c.name == 'long_int_range' }
|
||||
end
|
||||
|
||||
def teardown
|
||||
@connection.execute 'drop table if exists intrange_data_type'
|
||||
end
|
||||
|
||||
def test_columns
|
||||
assert_equal :intrange, @int_range_column.type
|
||||
assert_equal :intrange, @long_int_range_column.type
|
||||
end
|
||||
|
||||
def test_type_cast_intrange
|
||||
assert @int_range_column
|
||||
assert_equal(true, @int_range_column.has_default?)
|
||||
assert_equal((1..10), @int_range_column.default)
|
||||
assert_equal("int4range", @int_range_column.sql_type)
|
||||
|
||||
data = "[1,10)"
|
||||
hash = @int_range_column.class.string_to_intrange data
|
||||
assert_equal((1..9), hash)
|
||||
assert_equal((1..9), @int_range_column.type_cast(data))
|
||||
|
||||
assert_equal((nil..nil), @int_range_column.type_cast("empty"))
|
||||
assert_equal((1..5), @int_range_column.type_cast('[1,5]'))
|
||||
assert_equal((2..4), @int_range_column.type_cast('(1,5)'))
|
||||
assert_equal((2..39), @int_range_column.type_cast('[2,40)'))
|
||||
assert_equal((10..20), @int_range_column.type_cast('(9,20]'))
|
||||
end
|
||||
|
||||
def test_type_cast_long_intrange
|
||||
assert @long_int_range_column
|
||||
assert_equal(true, @long_int_range_column.has_default?)
|
||||
assert_equal((1..100), @long_int_range_column.default)
|
||||
assert_equal("int8range", @long_int_range_column.sql_type)
|
||||
end
|
||||
|
||||
def test_rewrite
|
||||
@connection.execute "insert into intrange_data_type (int_range) VALUES ('(1, 6)')"
|
||||
x = IntRangeDataType.first
|
||||
x.int_range = (1..100)
|
||||
assert x.save!
|
||||
end
|
||||
|
||||
def test_select
|
||||
@connection.execute "insert into intrange_data_type (int_range) VALUES ('(1, 4]')"
|
||||
x = IntRangeDataType.first
|
||||
assert_equal((2..4), x.int_range)
|
||||
end
|
||||
|
||||
def test_empty_range
|
||||
@connection.execute %q|insert into intrange_data_type (int_range) VALUES('empty')|
|
||||
x = IntRangeDataType.first
|
||||
assert_equal((nil..nil), x.int_range)
|
||||
end
|
||||
|
||||
def test_rewrite_to_nil
|
||||
@connection.execute %q|insert into intrange_data_type (int_range) VALUES('(1, 4]')|
|
||||
x = IntRangeDataType.first
|
||||
x.int_range = nil
|
||||
assert x.save!
|
||||
assert_equal(nil, x.int_range)
|
||||
end
|
||||
|
||||
def test_invalid_intrange
|
||||
assert IntRangeDataType.create!(int_range: ('a'..'d'))
|
||||
x = IntRangeDataType.first
|
||||
assert_equal(nil, x.int_range)
|
||||
end
|
||||
|
||||
def test_save_empty_range
|
||||
assert IntRangeDataType.create!(int_range: (nil..nil))
|
||||
x = IntRangeDataType.first
|
||||
assert_equal((nil..nil), x.int_range)
|
||||
end
|
||||
|
||||
def test_save_invalid_data
|
||||
assert_raises(ActiveRecord::StatementInvalid) do
|
||||
IntRangeDataType.create!(int_range: "empty1")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
ActiveRecord::Schema.define do
|
||||
|
||||
%w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
|
||||
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_intrange_data_type).each do |table_name|
|
||||
%w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
|
||||
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
|
||||
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
|
||||
end
|
||||
|
||||
|
@ -73,6 +73,18 @@ _SQL
|
|||
);
|
||||
_SQL
|
||||
|
||||
execute <<_SQL
|
||||
CREATE TABLE postgresql_ranges (
|
||||
id SERIAL PRIMARY KEY,
|
||||
date_range daterange,
|
||||
num_range numrange,
|
||||
ts_range tsrange,
|
||||
tstz_range tstzrange,
|
||||
int4_range int4range,
|
||||
int8_range int8range
|
||||
);
|
||||
_SQL
|
||||
|
||||
execute <<_SQL
|
||||
CREATE TABLE postgresql_tsvectors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
@ -106,16 +118,6 @@ _SQL
|
|||
);
|
||||
_SQL
|
||||
end
|
||||
|
||||
if 't' == select_value("select 'int4range'=ANY(select typname from pg_type)")
|
||||
execute <<_SQL
|
||||
CREATE TABLE postgresql_intrange_data_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
int_range int4range,
|
||||
int_long_range int8range
|
||||
);
|
||||
_SQL
|
||||
end
|
||||
|
||||
execute <<_SQL
|
||||
CREATE TABLE postgresql_moneys (
|
||||
|
|
Loading…
Reference in a new issue