Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

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

Merged
merged 1 commit into from

3 participants

@le0pard

Hello all!

The PostgreSQL version 9.2 and higher supports a Range Types. I think that would be nice if AR supports its too. For now I just finished intrange type.

Thanks.

@rafaelfranca rafaelfranca merged commit cc74088 into from
@rafaelfranca

thank you

@le0pard

Big thanks, @rafaelfranca! Can you also check #8542, because I add tests for invalid data and ranges?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
4 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*
View
1  activerecord/lib/active_record/connection_adapters/column.rb
@@ -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
View
23 activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -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
View
11 activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -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 = {}
@@ -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
View
10 activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -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)
@@ -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)
View
8 activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -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
View
19 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -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
@@ -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
@@ -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]
@@ -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
View
87 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
View
12 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
@@ -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 (
Something went wrong with that request. Please try again.