aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorAlexey <leopard.not.a@gmail.com>2012-12-16 04:10:58 +0200
committerAlexey <leopard.not.a@gmail.com>2012-12-16 13:11:10 +0200
commit9a4a095ed7ea6f0f65cc9e3bf3258cbdd0ddc210 (patch)
treeb5d59edd3a6b8476f6cb09465fe5dc80e66c892a /activerecord
parent2d6abcce55c9024e2ef8753ab710b8a053e96eec (diff)
downloadrails-9a4a095ed7ea6f0f65cc9e3bf3258cbdd0ddc210.tar.gz
rails-9a4a095ed7ea6f0f65cc9e3bf3258cbdd0ddc210.tar.bz2
rails-9a4a095ed7ea6f0f65cc9e3bf3258cbdd0ddc210.zip
AR supporting new intrange data type on PostgreSQL >= 9.2
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md4
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb19
-rw-r--r--activerecord/test/cases/adapters/postgresql/intrange_test.rb87
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb12
9 files changed, 170 insertions, 5 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 97b7b8cd1e..11e23ad0f4 100644
--- a/activerecord/CHANGELOG.md
+++ b/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*
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index df23dbfb60..fd36a5b075 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -126,6 +126,7 @@ 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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index c04a799b8d..3772f7ddaa 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -92,6 +92,29 @@ 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 Range === object
+ "[#{object.first},#{object.exclude_end? ? object.last : object.last.to_i + 1})"
+ else
+ object
+ end
+ end
+
private
HstorePair = begin
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 52344f61c0..18ea83ce42 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -168,6 +168,14 @@ 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 = {}
@@ -269,6 +277,9 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 62a4d76928..c2fcef94da 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -31,6 +31,11 @@ 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)
@@ -89,6 +94,11 @@ 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)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 18bf14d1fb..e10b562fa4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -417,6 +417,14 @@ 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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index e24ee1efdd..d62a375470 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -114,6 +114,9 @@ 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
@@ -209,9 +212,12 @@ module ActiveRecord
# 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 @@ 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]
@@ -329,7 +339,8 @@ module ActiveRecord
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
- json: { name: "json" }
+ json: { name: "json" },
+ intrange: { name: "int4range" }
}
include Quoting
diff --git a/activerecord/test/cases/adapters/postgresql/intrange_test.rb b/activerecord/test/cases/adapters/postgresql/intrange_test.rb
new file mode 100644
index 0000000000..affacd092a
--- /dev/null
+++ b/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
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index ab2a63d3ea..96fef3a831 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/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
);
_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 (