Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AR supporting new int4range and int8range data type on PostgreSQL >= 9.2 #8528

Merged
merged 1 commit into from Dec 17, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,5 +1,9 @@
## Rails 4.0.0 (unreleased) ##

* Allow int4range and int8range columns to be created in PostgreSQL and properly convert to/from database.

*Alexey Vasiliev aka leopard*

* Do not log the binding values for binary columns.

*Matthew M. Boedicker*
Expand Down
Expand Up @@ -126,6 +126,7 @@ 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 @@ -92,6 +92,29 @@ 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 Range === object
"[#{object.first},#{object.exclude_end? ? object.last : object.last.to_i + 1})"
else
object
end
end

private

HstorePair = begin
Expand Down
Expand Up @@ -168,6 +168,14 @@ 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 @@ -269,6 +277,9 @@ def self.registered_type?(name)
register_type 'hstore', OID::Hstore.new
register_type 'json', OID::Json.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 @@ -31,6 +31,11 @@ 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 @@ -89,6 +94,11 @@ 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,6 +417,14 @@ 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 @@ -114,6 +114,9 @@ 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 @@ -209,9 +212,12 @@ def simplified_type(field_type)
# UUID type
when 'uuid'
:uuid
# JSON type
when 'json'
:json
# JSON type
when 'json'
:json
# int4range, int8range types
when 'int4range', 'int8range'
:intrange
# Small and big integer types
when /^(?:small|big)int$/
:integer
Expand Down Expand Up @@ -289,6 +295,10 @@ 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 @@ -329,7 +339,8 @@ def new_column_definition(base, name, type)
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" }
json: { name: "json" },
intrange: { name: "int4range" }
}

include Quoting
Expand Down
87 changes: 87 additions & 0 deletions activerecord/test/cases/adapters/postgresql/intrange_test.rb
@@ -0,0 +1,87 @@
# 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.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
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

end
12 changes: 11 additions & 1 deletion activerecord/test/schema/postgresql_specific_schema.rb
@@ -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_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|
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|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end

Expand Down Expand Up @@ -97,6 +97,16 @@
);
_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 (
Expand Down