diff options
Diffstat (limited to 'activerecord')
23 files changed, 484 insertions, 270 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f1cca0ad76..278baee68f 100644 --- a/activerecord/CHANGELOG.md +++ b/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. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 3675184193..847d9da6e6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -237,7 +237,7 @@ module ActiveRecord @spec = spec @checkout_timeout = spec.config[:checkout_timeout] || 5 - @dead_connection_timeout = spec.config[:dead_connection_timeout] + @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5 @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 9d6111b51e..fd5eaab9c9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index fb28ecb6cf..747331f3a1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index f7d734a2f1..3d8f0b575c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 02c295983f..d90b9283ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c2fcef94da..791b032023 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -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) 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 8c68576bdc..73ca2c8e61 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index b1b0467379..209553b26e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -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" } } @@ -552,6 +575,11 @@ module ActiveRecord true end + # Range datatypes weren't introduced until PostgreSQL 9.2 + def supports_ranges? + postgresql_version >= 90200 + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 883d25d80b..537ebbef28 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -58,7 +58,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first.to_sym if key.include?('.') + key.split('.').first if key.include?('.') end end.compact end @@ -66,7 +66,7 @@ module ActiveRecord private def self.build(attribute, value) case value - when Array, ActiveRecord::Associations::CollectionProxy + when Array values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 46c0d6206f..42849d6bc9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -764,6 +764,11 @@ module ActiveRecord [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + + attributes.values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 2254be8612..7351ed9013 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -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,106 @@ 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 if @connection.supports_ranges? + 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 if @connection.supports_ranges? + 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 if @connection.supports_ranges? + 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 if @connection.supports_ranges? + INSERT INTO postgresql_ranges ( + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range + ) VALUES ( + '[,]', + '[,]', + '[,]', + '[,]', + '[,]', + '[,]' + ) +_SQL + + @connection.execute <<_SQL if @connection.supports_ranges? + 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 + + if @connection.supports_ranges? + @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) + end + @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 +184,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :text, @first_array.column_for_attribute(:nicknames).type end + def test_data_type_of_range_types + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 +240,201 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal "'text' 'vector'", @first_tsvector.text_vector end + def test_int4range_values + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 + skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? + 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 diff --git a/activerecord/test/cases/adapters/postgresql/intrange_test.rb b/activerecord/test/cases/adapters/postgresql/intrange_test.rb deleted file mode 100644 index 5f6a64619d..0000000000 --- a/activerecord/test/cases/adapters/postgresql/intrange_test.rb +++ /dev/null @@ -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 diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb deleted file mode 100644 index 44aaa65416..0000000000 --- a/activerecord/test/cases/associations/habtm_join_table_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'cases/helper' - -class MyReader < ActiveRecord::Base - has_and_belongs_to_many :my_books -end - -class MyBook < ActiveRecord::Base - has_and_belongs_to_many :my_readers -end - -class HabtmJoinTableTest < ActiveRecord::TestCase - def test_habtm_join_table - ActiveRecord::Base.connection.create_table :my_books, :force => true do |t| - t.string :name - end - assert ActiveRecord::Base.connection.table_exists?(:my_books) - - ActiveRecord::Base.connection.create_table :my_readers, :force => true do |t| - t.string :name - end - assert ActiveRecord::Base.connection.table_exists?(:my_readers) - - ActiveRecord::Base.connection.create_table :my_books_my_readers, :force => true do |t| - t.integer :my_book_id - t.integer :my_reader_id - end - assert ActiveRecord::Base.connection.table_exists?(:my_books_my_readers) - end - - def teardown - ActiveRecord::Base.connection.drop_table :my_books - ActiveRecord::Base.connection.drop_table :my_readers - ActiveRecord::Base.connection.drop_table :my_books_my_readers - end -end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 5ffb32e809..7dbb6616f8 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -15,6 +15,8 @@ require 'support/connection' # TODO: Move all these random hacks into the ARTest namespace and into the support/ dir +Thread.abort_on_exception = true + # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index f69a248491..c43c7601a2 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -8,7 +8,20 @@ require 'models/edge' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges + fixtures :posts, :edges, :authors + + def test_where_copies_bind_params + author = authors(:david) + posts = author.posts.where('posts.id != 1') + joined = Post.where(id: posts) + + assert_operator joined.length, :>, 0 + + joined.each { |post| + assert_equal author, post.author + assert_not_equal 1, post.id + } + end def test_belongs_to_shallow_where author = Author.new diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 6935cfb0ea..83904cd850 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -9,17 +9,7 @@ class Author < ActiveRecord::Base has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post" has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post" has_many :posts_containing_the_letter_a, :class_name => "Post" - has_many :posts_with_extension, :class_name => "Post" do #, :extend => ProxyTestExtension - def testing_proxy_owner - proxy_owner - end - def testing_proxy_reflection - proxy_reflection - end - def testing_proxy_target - proxy_target - end - end + has_many :posts_with_extension, :class_name => "Post" has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post' has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post' has_many :comments, :through => :posts diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index f8c8ebb70c..7da39a8e33 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -31,9 +31,4 @@ class Category < ActiveRecord::Base end class SpecialCategory < Category - - def self.what_are_you - 'a special category...' - end - end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 4b2015fe01..ede5fbd0c6 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -29,16 +29,10 @@ class Comment < ActiveRecord::Base end class SpecialComment < Comment - def self.what_are_you - 'a special comment...' - end end class SubSpecialComment < SpecialComment end class VerySpecialComment < Comment - def self.what_are_you - 'a very special comment...' - end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 603f1f2555..73ffc0de38 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -16,11 +16,7 @@ class Post < ActiveRecord::Base scope :limit_by, lambda {|l| limit(l) } - belongs_to :author do - def greeting - "hello" - end - end + belongs_to :author belongs_to :author_with_posts, -> { includes(:posts) }, :class_name => "Author", :foreign_key => :author_id belongs_to :author_with_address, -> { includes(:author_address) }, :class_name => "Author", :foreign_key => :author_id @@ -168,18 +164,6 @@ class SubStiPost < StiPost self.table_name = Post.table_name end -ActiveSupport::Deprecation.silence do - class DeprecatedPostWithComment < ActiveRecord::Base - self.table_name = 'posts' - default_scope where("posts.comments_count > 0").order("posts.comments_count ASC") - end -end - -class PostForAuthor < ActiveRecord::Base - self.table_name = 'posts' - cattr_accessor :selected_author -end - class FirstPost < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => 1) } diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index af3ec4be83..90273adafc 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -40,7 +40,4 @@ class Project < ActiveRecord::Base end class SpecialProject < Project - def hello_world - "hello there!" - end end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index f7f4cebc5a..17035bf338 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -108,6 +108,7 @@ class ImportantTopic < Topic end class BlankTopic < Topic + # declared here to make sure that dynamic finder with a bang can find a model that responds to `blank?` def blank? true end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index ae13f2cd8a..83b50030bd 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_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 if supports_ranges? + 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 ( |