diff options
Diffstat (limited to 'activerecord/lib/active_record')
39 files changed, 394 insertions, 202 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 16a46a59d1..06bdabfced 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -191,7 +191,7 @@ module ActiveRecord # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> # <tt>Project#milestones.build, Project#milestones.create</tt> # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 300f67959d..c5fb1fe2c7 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -15,7 +15,6 @@ module ActiveRecord def scope scope = klass.unscoped - scope.merge! eval_scope(klass, reflection.scope) if reflection.scope scope.extending! Array(options[:extend]) add_constraints(scope) end @@ -59,7 +58,7 @@ module ActiveRecord if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] - key = reflection.association_primary_key(klass) + key = reflection.association_primary_key(self.klass) else key = reflection.association_primary_key end @@ -92,8 +91,13 @@ module ActiveRecord # Exclude the scope of the association itself, because that # was already merged in the #scope method. - (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item) + scope_chain[i].each do |scope_chain_item| + klass = i == 0 ? self.klass : reflection.klass + item = eval_scope(klass, scope_chain_item) + + if scope_chain_item == self.reflection.scope + scope.merge! item.except(:where, :includes) + end scope.includes! item.includes_values scope.where_values += item.where_values diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index cd366ac8b7..f40368cfeb 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -68,7 +68,7 @@ module ActiveRecord remove_duplicate_results!(base, records, association) end when Hash - associations.keys.each do |name| + associations.each_key do |name| reflection = base.reflections[name] remove_uniq_by_reflection(reflection, records) diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 47a8b576c0..41b5a6e926 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -33,20 +33,12 @@ module ActiveRecord def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(original_time) - original_time = nil if original_time.blank? - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - zoned_time = time && time.in_time_zone rescue nil - rounded_time = round_usec(zoned_time) - rounded_value = round_usec(read_attribute("#{attr_name}")) - if (rounded_value != rounded_time) || (!rounded_value && original_time) - write_attribute("#{attr_name}", original_time) - #{attr_name}_will_change! - @attributes_cache["#{attr_name}"] = zoned_time - end + def #{attr_name}=(time) + time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil + previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) + write_attribute(:#{attr_name}, time) + #{attr_name}_will_change! if previous_time != time_with_zone + @attributes_cache["#{attr_name}"] = time_with_zone end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -62,11 +54,6 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end - - private - def round_usec(value) - value.change(usec: 0) if value - end end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index bf5793d454..e262401da6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -162,8 +162,8 @@ module ActiveRecord #:nodoc: # # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects # by simple queries without turning to SQL. They work by appending the name of an attribute - # to <tt>find_by_</tt> # like <tt>Person.find_by_user_name</tt>. - # Instead of writing # <tt>Person.where(user_name: user_name).first</tt>, you just do + # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>. + # Instead of writing <tt>Person.where(user_name: user_name).first</tt>, you just do # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an 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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index be6fda95b4..41e07fbda9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -9,9 +9,9 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 - def #{method_name}(*) # def update_with_query_dirty(*args) + def #{method_name}(*) # def update_with_query_dirty(*) clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - super # update_without_query_dirty(*args) + super # super end # end end_code end @@ -85,6 +85,8 @@ module ActiveRecord end end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such + # queries should not be cached. def locked?(arel) arel.respond_to?(:locked) && arel.locked end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 60a9eee7c7..aec4654eee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -25,13 +25,19 @@ module ActiveRecord when true, false if column && column.type == :integer value ? '1' : '0' + elsif column && [:text, :string, :binary].include?(column.type) + value ? "'1'" : "'0'" else value ? quoted_true : quoted_false end # BigDecimals need to be put in a non-normalized form and quoted. when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s + when Numeric, ActiveSupport::Duration + value = BigDecimal === value ? value.to_s('F') : value.to_s + if column && ![:integer, :float, :decimal].include?(column.type) + value = "'#{value}'" + end + value when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value.to_s}'" @@ -93,6 +99,18 @@ module ActiveRecord quote_column_name(table_name) end + # Override to return the quoted table name for assignment. Defaults to + # table quoting. + # + # This works for mysql and mysql2 where table.column can be used to + # resolve ambiguity. + # + # We override this in the sqlite and postgresql adapters to use only + # the column name (as per syntax requirements). + def quote_table_name_for_assignment(table, attr) + quote_table_name("#{table}.#{attr}") + end + def quoted_true "'t'" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index b1ec33d06c..f758e19a4f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -252,15 +252,12 @@ module ActiveRecord self end - %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| - class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - column_names = args # column_names = args - type = :'#{column_type}' # type = :string - column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) } - end # end - EOV + [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + define_method column_type do |*args| + options = args.extract_options! + column_names = args + column_names.each { |name| column(name, column_type, options) } + end end # Adds index options to the indexes hash, keyed by column name @@ -486,15 +483,13 @@ module ActiveRecord # # t.string(:goat) # t.string(:goat, :sheep) - %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| - class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - args.each do |name| # column_names.each do |name| - @base.add_column(@table_name, name, :#{column_type}, options) # @base.add_column(@table_name, name, :string, options) - end # end - end # end - EOV + [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + define_method column_type do |*args| + options = args.extract_options! + args.each do |name| + @base.add_column(@table_name, name, column_type, options) + end + end end private 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/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2b8026dbf9..3ecef96b10 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -5,17 +5,36 @@ module ActiveRecord def initialize(connection) @connection = connection - @state = nil + @state = TransactionState.new + end + + def state + @state + end + end + + class TransactionState + + VALID_STATES = Set.new([:committed, :rolledback, nil]) + + def initialize(state = nil) + @state = state end def committed? - @state == :commit + @state == :committed end def rolledback? - @state == :rollback + @state == :rolledback end + def set_state(state) + if !VALID_STATES.include?(state) + raise ArgumentError, "Invalid transaction state: #{state}" + end + @state = state + end end class ClosedTransaction < Transaction #:nodoc: @@ -101,7 +120,7 @@ module ActiveRecord end def rollback_records - @state = :rollback + @state.set_state(:rolledback) records.uniq.each do |record| begin record.rolledback!(parent.closed?) @@ -112,7 +131,7 @@ module ActiveRecord end def commit_records - @state = :commit + @state.set_state(:committed) records.uniq.each do |record| begin record.committed! diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 9cdca020dd..eecf4faa5d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -171,10 +171,22 @@ module ActiveRecord false end + # Does this adapter support database extensions? As of this writing + # only postgresql does. + def supports_extensions? + false + end + + # A list of extensions, to be filled in by databases that + # support them (at the moment, postgresql). + def extensions + [] + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current - # +binds+ + # +binds+. def substitute_at(column, index) Arel::Nodes::BindParam.new '?' end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 25860d5fae..de5232f960 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -212,8 +212,6 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") else super 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/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 09250d3c01..577a362568 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -62,6 +62,10 @@ module ActiveRecord ConnectionSpecification.new(spec, adapter_method) end + # For DATABASE_URL, accept a limited concept of ints and floats + SIMPLE_INT = /\A\d+\z/ + SIMPLE_FLOAT = /\A\d+\.\d+\z/ + def connection_url_to_hash(url) # :nodoc: config = URI.parse url adapter = config.scheme @@ -72,15 +76,38 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } + spec.reject!{ |_,value| value.blank? } + uri_parser = URI::Parser.new + spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } + if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + + options.each { |key, value| options[key] = type_cast_value(value) } + spec.merge!(options) end + spec end + + def type_cast_value(value) + case value + when SIMPLE_INT + value.to_i + when SIMPLE_FLOAT + value.to_f + when 'true' + true + when 'false' + false + else + value + end + end end 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..e09319890a 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 extract_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 = extract_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 = {} @@ -189,6 +239,10 @@ module ActiveRecord @mapping[oid] end + def clear + @mapping.clear + end + def key?(oid) @mapping.key? oid end @@ -241,6 +295,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 +339,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..47e2e3928f 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) @@ -131,6 +130,10 @@ module ActiveRecord end end + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + # Quotes column names for use in SQL queries. def quote_column_name(name) #:nodoc: PGconn.quote_ident(name.to_s) 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 129a3e7487..271a6848ee 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,45 @@ module ActiveRecord true end + # Returns true if pg > 9.2 + def supports_extensions? + postgresql_version >= 90200 + end + + # Range datatypes weren't introduced until PostgreSQL 9.2 + def supports_ranges? + postgresql_version >= 90200 + end + + def enable_extension(name) + exec_query("CREATE EXTENSION IF NOT EXISTS #{name}").tap { + reload_type_map + } + end + + def disable_extension(name) + exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + reload_type_map + } + end + + def extension_enabled?(name) + if supports_extensions? + res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL)", + 'SCHEMA' + res.column_types['exists'].type_cast res.rows.first.first + end + end + + def extensions + if supports_extensions? + res = exec_query "SELECT extname from pg_extension", "SCHEMA" + res.rows.map { |r| res.column_types['extname'].type_cast r.first } + else + super + end + 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 @@ -617,6 +679,11 @@ module ActiveRecord private + def reload_type_map + OID::TYPE_MAP.clear + initialize_type_map + end + def initialize_type_map result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 8a2e7775d5..b644e7bd60 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -229,6 +229,10 @@ module ActiveRecord @connection.class.quote(s) end + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + def quote_column_name(name) #:nodoc: %Q("#{name.to_s.gsub('"', '""')}") end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 812f1ce5c5..63a1197a56 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -365,18 +365,18 @@ module ActiveRecord pk = self.class.primary_key @attributes[pk] = nil unless @attributes.key?(pk) - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true - @txn = nil - @_start_transaction_state = {} - @transaction = nil + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @txn = nil + @_start_transaction_state = {} + @transaction = nil end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 5a4ef5991d..2958d08210 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -363,11 +363,11 @@ module ActiveRecord # # first: # name: Smurf - # *DEFAULTS + # <<: *DEFAULTS # # second: # name: Fraggle - # *DEFAULTS + # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 67339c05e5..823595a128 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -342,7 +342,7 @@ module ActiveRecord end def call(env) - ActiveRecord::Base.logger.quietly do + ActiveRecord::Base.logger.silence do ActiveRecord::Migration.check_pending! end @app.call(env) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 1b2aa9349e..803cae7115 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -220,7 +220,7 @@ module ActiveRecord save end end - + alias update_attributes update # Updates its receiver just like +update+ but calls <tt>save!</tt> instead @@ -233,7 +233,7 @@ module ActiveRecord save! end end - + alias update_attributes! update! # Equivalent to <code>update_columns(name => value)</code>. diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 5ddcaee6be..f08b9c614d 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 259d0ff12b..f36af7182f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -284,8 +284,6 @@ db_namespace = namespace :db do filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config case current_config['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) when 'oci', 'oracle' ActiveRecord::Base.establish_connection(current_config) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } @@ -296,7 +294,7 @@ db_namespace = namespace :db do db_string = firebird_db_string(current_config) sh "isql -a #{db_string} > #{filename}" else - raise "Task not supported by '#{current_config["adapter"]}'" + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) end if ActiveRecord::Base.connection.supports_migrations? @@ -312,8 +310,6 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case current_config['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) when 'sqlserver' `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` when 'oci', 'oracle' @@ -326,7 +322,7 @@ db_namespace = namespace :db do db_string = firebird_db_string(current_config) sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{current_config['adapter']}'" + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) end end @@ -384,8 +380,6 @@ db_namespace = namespace :db do task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -401,7 +395,7 @@ db_namespace = namespace :db do ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.recreate_database! else - raise "Task not supported by '#{abcs['test']['adapter']}'" + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index bcfcb061f2..0995750ecd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 3f154bd1cc..f10c290755 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -192,7 +192,8 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - distinct = options[:distinct] + # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) + distinct = options[:distinct] || self.uniq_value if operation == "count" column_name ||= (select_for_count || :all) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 59226d316e..eb23e92fb8 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require "set" module ActiveRecord class Relation @@ -105,25 +106,26 @@ module ActiveRecord end def merged_wheres - if values[:where] - merged_wheres = relation.where_values + values[:where] - - unless relation.where_values.empty? - # Remove equalities with duplicated left-hand. Last one wins. - seen = {} - merged_wheres = merged_wheres.reverse.reject { |w| - nuke = false - if w.respond_to?(:operator) && w.operator == :== - nuke = seen[w.left] - seen[w.left] = true - end - nuke - }.reverse - end + values[:where] ||= [] - merged_wheres + if values[:where].empty? || relation.where_values.empty? + relation.where_values + values[:where] else - relation.where_values + # Remove equalities from the existing relation with a LHS which is + # present in the relation being merged in. + + seen = Set.new + values[:where].each { |w| + if w.respond_to?(:operator) && w.operator == :== + seen << w.left + end + } + + relation.where_values.reject { |w| + w.respond_to?(:operator) && + w.operator == :== && + seen.include?(w.left) + } + values[:where] end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 883d25d80b..5cd015eba7 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -8,7 +8,7 @@ module ActiveRecord if value.is_a?(Hash) if value.empty? - queries << '1 = 2' + queries << '1=0' else table = Arel::Table.new(column, default_table.engine) association = klass.reflect_on_association(column.to_sym) @@ -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)} @@ -98,6 +98,11 @@ module ActiveRecord when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) + when Integer, ActiveSupport::Duration + # Arel treats integers as literals, but they should be quoted when compared with strings + table = attribute.relation + column = table.engine.connection.schema_cache.columns_hash(table.name)[attribute.name.to_s] + attribute.eq(Arel::Nodes::SqlLiteral.new(table.engine.connection.quote(value, column))) else attribute.eq(value) end 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/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2dad1dc177..3c5b871e99 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -33,10 +33,10 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'" - def sanitize_sql_for_assignment(assignments) + def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) case assignments when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) else assignments end end @@ -98,9 +98,9 @@ module ActiveRecord # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs) + def sanitize_sql_hash_for_assignment(attrs, table) attrs.map do |attr, value| - "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" + "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}" end.join(', ') end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 4929103f15..fa9de926c5 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -24,6 +24,7 @@ module ActiveRecord def dump(stream) header(stream) + extensions(stream) tables(stream) trailer(stream) stream @@ -66,6 +67,18 @@ HEADER stream.puts "end" end + def extensions(stream) + return unless @connection.supports_extensions? + extensions = @connection.extensions + if extensions.any? + stream.puts " # These are extensions that must be enabled in order to support this database" + extensions.each do |extension| + stream.puts " enable_extension #{extension.inspect}" + end + stream.puts + end + end + def tables(stream) @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 0c3fd1bd29..9746b1c3c2 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Scoping extend ActiveSupport::Concern @@ -25,6 +24,5 @@ module ActiveRecord send("#{att}=", value) if respond_to?("#{att}=") end end - end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 6835d0e01b..5bd481082e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Scoping module Default @@ -99,7 +98,7 @@ module ActiveRecord ) end - self.default_scopes = default_scopes + [scope] + self.default_scopes += [scope] end def build_default_scope # :nodoc: @@ -140,7 +139,6 @@ module ActiveRecord self.ignore_default_scope = false end end - end end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index cf4cf9e602..a610f479f2 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -42,21 +42,19 @@ module ActiveRecord # # All stored values are automatically available through accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting - # the default accessors (using the same name as the attribute) and calling - # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and - # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually - # change things. + # the default accessors (using the same name as the attribute) and calling <tt>super</tt> + # to actually change things. # # class Song < ActiveRecord::Base # # Uses a stored integer to hold the volume adjustment of the song # store :settings, accessors: [:volume_adjustment] # # def volume_adjustment=(decibels) - # write_store_attribute(:settings, :volume_adjustment, decibels.to_i) + # super(decibels.to_i) # end # # def volume_adjustment - # read_store_attribute(:settings, :volume_adjustment).to_i + # super.to_i # end # end module Store @@ -75,19 +73,30 @@ module ActiveRecord def store_accessor(store_attribute, *keys) keys = keys.flatten - keys.each do |key| - define_method("#{key}=") do |value| - write_store_attribute(store_attribute, key, value) - end - define_method(key) do - read_store_attribute(store_attribute, key) + _store_accessors_module.module_eval do + keys.each do |key| + define_method("#{key}=") do |value| + write_store_attribute(store_attribute, key, value) + end + + define_method(key) do + read_store_attribute(store_attribute, key) + end end end self.stored_attributes[store_attribute] ||= [] self.stored_attributes[store_attribute] |= keys end + + def _store_accessors_module + @_store_accessors_module ||= begin + mod = Module.new + include mod + mod + end + end end protected diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 67c7e714e6..4fa7cf8a7d 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,6 +1,7 @@ module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: + class DatabaseNotSupported < StandardError; end # :nodoc: module DatabaseTasks # :nodoc: extend self @@ -121,6 +122,9 @@ module ActiveRecord def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } + unless key + raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" + end @tasks[key] end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index c035ad43a2..e9142481a3 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -60,16 +60,17 @@ module ActiveRecord self.clear_log - self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] + self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] + sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] - [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| ignored_sql.concat db_ignored_sql end |