diff options
Diffstat (limited to 'activerecord/lib')
38 files changed, 801 insertions, 284 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0248c7483c..84540a7000 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,12 +5,13 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :table_joins + attr_reader :aliases, :table_joins, :connection # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(table_joins = []) + def initialize(connection = ActiveRecord::Model.connection, table_joins = []) @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins + @connection = connection end def aliased_table_for(table_name, aliased_name = nil) @@ -70,10 +71,6 @@ module ActiveRecord def truncate(name) name.slice(0, connection.table_alias_length - 2) end - - def connection - ActiveRecord::Base.connection - end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 0209ce36df..982084c9b8 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -10,7 +10,7 @@ module ActiveRecord def initialize(association) @association = association - @alias_tracker = AliasTracker.new + @alias_tracker = AliasTracker.new klass.connection end def scope diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ba01df00e3..5eda0387c4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -82,9 +82,8 @@ module ActiveRecord proxy_association.send :add_to_target, r yield(r) if block_given? end - end - if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) + elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) if load_target if target.respond_to?(method) target.send(method, *args, &block) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 827b01c5ac..cd366ac8b7 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -13,7 +13,7 @@ module ActiveRecord @join_parts = [JoinBase.new(base)] @associations = {} @reflections = [] - @alias_tracker = AliasTracker.new(joins) + @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index a1e34a3aa1..3e27e85f02 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -194,6 +194,7 @@ module ActiveRecord # Returns the column object for the named attribute. def column_for_attribute(name) + # FIXME: should this return a null object for columns that don't exist? self.class.columns_hash[name.to_s] end @@ -243,7 +244,7 @@ module ActiveRecord end def attribute_method?(attr_name) - attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + defined?(@attributes) && @attributes.include?(attr_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 7c59664703..2e1a2dc3ef 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -24,6 +24,12 @@ module ActiveRecord query_attribute(self.class.primary_key) end + protected + + def attribute_method?(attr_name) + attr_name == 'id' || super + end + module ClassMethods def define_method_attribute(attr_name) super diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 948809c65a..1e841dc8e0 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -10,8 +10,11 @@ module ActiveRecord end def query_attribute(attr_name) - unless value = read_attribute(attr_name) - false + value = read_attribute(attr_name) + + case value + when true then true + when false, nil then false else column = self.class.columns_hash[attr_name] if column.nil? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index c129dc8c52..846ac03d82 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -88,19 +88,15 @@ module ActiveRecord private def cacheable_column?(column) - attribute_types_cached_by_default.include?(column.type) + if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + ! serialized_attributes.include? column.name + else + attribute_types_cached_by_default.include?(column.type) + end end def internal_attribute_access_code(attr_name, cast_code) - access_code = "v = @attributes.fetch(attr_name) { missing_attribute(attr_name, caller) };" - - access_code << "v && #{cast_code};" - - if cache_attribute?(attr_name) - access_code = "@attributes_cache[attr_name] ||= (#{access_code})" - end - - "attr_name = '#{attr_name}'; #{access_code}" + "read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) }" end def external_attribute_access_code(attr_name, cast_code) @@ -121,13 +117,29 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache) + # If it's cached, just return it + @attributes_cache.fetch(attr_name) { |name| + column = @columns_hash.fetch(name) { + return self.class.type_cast_attribute(name, @attributes, @attributes_cache) + } + + value = @attributes.fetch(name) { + return block_given? ? yield(name) : nil + } + + if self.class.cache_attribute?(name) + @attributes_cache[name] = column.type_cast(value) + else + column.type_cast value + end + } end private - def attribute(attribute_name) - read_attribute(attribute_name) - end + + def attribute(attribute_name) + read_attribute(attribute_name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 0c8e4e4b9a..165785c8fb 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -10,6 +10,20 @@ module ActiveRecord self.serialized_attributes = {} end + class Type # :nodoc: + def initialize(column) + @column = column + end + + def type_cast(value) + value.unserialized_value + end + + def type + @column.type + end + end + class Attribute < Struct.new(:coder, :value, :state) def unserialized_value state == :serialized ? unserialize : value @@ -88,6 +102,14 @@ module ActiveRecord super end end + + def read_attribute_before_type_cast(attr_name) + if serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end + end end end end 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 2f86e32f41..20372c5c18 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -4,6 +4,21 @@ require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods module TimeZoneConversion + class Type # :nodoc: + def initialize(column) + @column = column + end + + def type_cast(value) + value = @column.type_cast(value) + value.acts_like?(:time) ? value.in_time_zone : value + end + + def type + @column.type + end + end + extend ActiveSupport::Concern included do @@ -16,46 +31,48 @@ module ActiveRecord module ClassMethods protected - # The enhanced read method automatically converts the UTC time stored in the database to the time - # zone stored in Time.zone. - def attribute_cast_code(attr_name) - column = columns_hash[attr_name] - - if create_time_zone_conversion_attribute?(attr_name, column) - typecast = "v = #{super}" - time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" - - "((#{typecast}) && (#{time_zone_conversion}))" - else - super - end + # The enhanced read method automatically converts the UTC time stored in the database to the time + # zone stored in Time.zone. + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" + else + super end + end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - 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) - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, original_time) - @attributes_cache["#{attr_name}"] = time + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. + 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) + time = original_time + unless time.acts_like?(:time) + time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super - end + time = time.in_time_zone rescue nil if time + write_attribute(:#{attr_name}, original_time) + @attributes_cache["#{attr_name}"] = time + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, line) + else + super end + end private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp]) - end + def create_time_zone_conversion_attribute?(name, column) + time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && + [:datetime, :timestamp].include?(column.type) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fde55b95da..50435921b1 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -28,6 +28,12 @@ module ActiveRecord @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) + # If we're dealing with a binary column, write the data to the cache + # so we don't attempt to typecast multiple times. + if column && column.binary? + @attributes_cache[attr_name] = value + end + if column || @attributes.has_key?(attr_name) @attributes[attr_name] = type_cast_attribute_for_write(column, value) else @@ -37,30 +43,16 @@ module ActiveRecord alias_method :raw_write_attribute, :write_attribute private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end - def type_cast_attribute_for_write(column, value) - if column && column.number? - convert_number_column_value(value) - else - value - end - end + def type_cast_attribute_for_write(column, value) + return value unless column - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end + column.type_cast_for_write value + end end end 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 132ca10f79..ad2e8634eb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end # Abstract representation of a column definition. Instances of this type diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 84c340770a..ea6071ea46 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -56,7 +56,7 @@ module ActiveRecord # Returns an array of Column objects for the table specified by +table_name+. # See the concrete implementation for details on the expected parameter values. - def columns(table_name, name = nil) end + def columns(table_name) end # Checks to see if a column exists in a given table. # @@ -381,9 +381,16 @@ module ActiveRecord # # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) # + # ====== Creating a partial index + # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active") + # generates + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # + # Note: only supported by PostgreSQL + # def add_index(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" + index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end # Remove the given index from the table. @@ -581,6 +588,9 @@ module ActiveRecord if Hash === options # legacy support, since this param was a string index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end else index_type = options end @@ -593,7 +603,7 @@ module ActiveRecord end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns] + [index_name, index_type, index_columns, index_options] end def index_name_for_remove(table_name, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index edea414db7..dd421b2054 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -142,6 +142,11 @@ module ActiveRecord false end + # Does this adapter support partial indices? + def supports_partial_index? + false + end + # Does this adapter support explain? As of this writing sqlite3, # mysql2, and postgresql are the only ones that do. def supports_explain? 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 9d9dbcc355..e1dad5b166 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -409,7 +409,7 @@ module ActiveRecord end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name, name = nil)#:nodoc: + def columns(table_name)#:nodoc: sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 2ecb198edb..78e54c4c9b 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -66,6 +66,25 @@ module ActiveRecord end end + def binary? + type == :binary + end + + # Casts a Ruby value to something appropriate for writing to the database. + def type_cast_for_write(value) + return value unless number? + + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end + # Casts value (which is a String) to an appropriate instance. def type_cast(value) return nil if value.nil? @@ -83,7 +102,6 @@ module ActiveRecord when :date then klass.value_to_date(value) when :binary then klass.binary_to_string(value) when :boolean then klass.value_to_boolean(value) - when :hstore then klass.cast_hstore(value) else value end end @@ -101,7 +119,7 @@ module ActiveRecord when :date then "#{klass}.value_to_date(#{var_name})" when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" - when :hstore then "#{klass}.cast_hstore(#{var_name})" + when :hstore then "#{klass}.string_to_hstore(#{var_name})" else var_name end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index c1332fde1a..321d500da2 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -80,6 +80,7 @@ module ActiveRecord disconnect! connect end + alias :reset! :reconnect! # Disconnects from the database if already connected. # Otherwise, this method does nothing. @@ -90,11 +91,6 @@ module ActiveRecord end end - def reset! - disconnect! - connect - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb new file mode 100644 index 0000000000..c82afc232c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -0,0 +1,243 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module OID + class Type + def type; end + + def type_cast_for_write(value) + value + end + end + + class Identity < Type + def type_cast(value) + value + end + end + + class Bytea < Type + def type_cast(value) + PGconn.unescape_bytea value + end + end + + class Money < Type + def type_cast(value) + return if value.nil? + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Vector < Type + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + + class Integer < Type + def type_cast(value) + return if value.nil? + + value.to_i rescue value ? 1 : 0 + end + end + + class Boolean < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_boolean value + end + end + + class Timestamp < Type + def type; :timestamp; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::PostgreSQLColumn.string_to_time value + end + end + + class Date < Type + def type; :datetime; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.value_to_date value + end + end + + class Time < Type + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.string_to_dummy_time value + end + end + + class Float < Type + def type_cast(value) + return if value.nil? + + value.to_f + end + end + + class Decimal < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Hstore < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + end + + class TypeMap + def initialize + @mapping = {} + end + + def []=(oid, type) + @mapping[oid] = type + end + + def [](oid) + @mapping[oid] + end + + def key?(oid) + @mapping.key? oid + end + + def fetch(ftype, fmod) + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if ftype == 1700 && (fmod - 4 & 0xffff).zero? + ftype = 23 + end + + @mapping.fetch(ftype) { |oid| yield oid, fmod } + end + end + + TYPE_MAP = TypeMap.new # :nodoc: + + # When the PG adapter connects, the pg_type table is queried. The + # key of this hash maps to the `typname` column from the table. + # TYPE_MAP is then dynamically built with oids as the key and type + # objects as values. + NAMES = Hash.new { |h,k| # :nodoc: + h[k] = OID::Identity.new + } + + # Register an OID type named +name+ with a typcasting object in + # +type+. +name+ should correspond to the `typname` column in + # the `pg_type` table. + def self.register_type(name, type) + NAMES[name] = type + end + + # Alias the +old+ type to the +new+ type. + def self.alias_type(new, old) + NAMES[new] = NAMES[old] + end + + # Is +name+ a registered type? + def self.registered_type?(name) + NAMES.key? name + end + + register_type 'int2', OID::Integer.new + alias_type 'int4', 'int2' + alias_type 'int8', 'int2' + alias_type 'oid', 'int2' + + register_type 'numeric', OID::Decimal.new + register_type 'text', OID::Identity.new + alias_type 'varchar', 'text' + alias_type 'char', 'text' + alias_type 'bpchar', 'text' + alias_type 'xml', 'text' + + # FIXME: why are we keeping these types as strings? + alias_type 'tsvector', 'text' + alias_type 'interval', 'text' + alias_type 'cidr', 'text' + alias_type 'inet', 'text' + alias_type 'macaddr', 'text' + alias_type 'bit', 'text' + alias_type 'varbit', 'text' + + # FIXME: I don't think this is correct. We should probably be returning a parsed date, + # but the tests pass with a string returned. + register_type 'timestamptz', OID::Identity.new + + register_type 'money', OID::Money.new + register_type 'bytea', OID::Bytea.new + register_type 'bool', OID::Boolean.new + + register_type 'float4', OID::Float.new + alias_type 'float8', 'float4' + + register_type 'timestamp', OID::Timestamp.new + register_type 'date', OID::Date.new + register_type 'time', OID::Time.new + + register_type 'path', OID::Identity.new + register_type 'polygon', OID::Identity.new + register_type 'circle', OID::Identity.new + register_type 'hstore', OID::Hstore.new + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 1d8e5d813a..fd5cbd3f9a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,6 +1,7 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/object/blank' require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/postgresql/oid' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' @@ -14,7 +15,7 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, + :schema_order, :adapter, :pool, :wait_timeout, :template, :reaping_frequency].each do |key| conn_params.delete key end @@ -34,7 +35,8 @@ module ActiveRecord # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: # Instantiates a new PostgreSQL column definition in a table. - def initialize(name, default, sql_type = nil, null = true) + def initialize(name, default, oid_type, sql_type = nil, null = true) + @oid_type = oid_type super(name, self.class.extract_value_from_default(default), sql_type, null) end @@ -52,180 +54,192 @@ module ActiveRecord end end - def cast_hstore(object) + def hstore_to_string(object) if Hash === object object.map { |k,v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ', ' + }.join ',' else - kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o| - unescape_hstore(o[1...-1]) - } - Hash[kvs.each_slice(2).to_a] + object end end - private - HSTORE_ESCAPE = { - ' ' => '\\ ', - '\\' => '\\\\', - '"' => '\\"', - '=' => '\\=', - } - HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys) - HSTORE_UNESCAPE = HSTORE_ESCAPE.invert - HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys) - - def unescape_hstore(value) - value.gsub(HSTORE_UNESCAPE_RE) do |match| - HSTORE_UNESCAPE[match] + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string end end + private + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + def escape_hstore(value) - value.gsub(HSTORE_ESCAPE_RE) do |match| - HSTORE_ESCAPE[match] - end + value.nil? ? 'NULL' + : value =~ /[=\s,>]/ ? '"%s"' % value.gsub(/(["\\])/, '\\\\\1') + : value == "" ? '""' + : value.to_s.gsub(/(["\\])/, '\\\\\1') end end # :startdoc: - private - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - else - super - end - end - - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore + # Extracts the value from a PostgreSQL column default definition. + def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + case default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + $1 # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string + when /\A'(.*)'::(?:character varying|bpchar|text)\z/m + $1 + # Character types (8.1 formatting) + when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m + $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } # Binary data types - when 'bytea' - :binary + when /\A'(.*)'::bytea\z/m + $1 # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string + when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ + $1 + when /\A'(.*)'::interval\z/ + $1 + # Boolean type + when 'true' + true + when 'false' + false # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string + when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ + $1 # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string + when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ + $1 + # Bit string types + when /\AB'(.*)'::"?bit(?: varying)?"?\z/ + $1 # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector + when /\A'(.*)'::xml\z/m + $1 # Arrays - when /^\D+\[\]$/ - :string + when /\A'(.*)'::"?\D+"?\[\]\z/ + $1 + # Hstore + when /\A'(.*)'::hstore\z/ + $1 # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :string - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. + when /\A-?\d+\z/ + $1 else - super - end + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end + end - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?)\z/ - $1 - # Character types - when /\A'(.*)'::(?:character varying|bpchar|text)\z/m - $1 - # Character types (8.1 formatting) - when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m - $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end + def type_cast(value) + return if value.nil? + return super if encoded? + + @oid_type.type_cast value + end + + private + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super end + end + + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end + + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + else + super + end + end + + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when 'interval' + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Network address types + when /^(?:cidr|inet|macaddr)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :string + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super + end + end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -284,7 +298,8 @@ module ActiveRecord :binary => { :name => "bytea" }, :boolean => { :name => "boolean" }, :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" } + :tsvector => { :name => "tsvector" }, + :hstore => { :name => "hstore" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -302,6 +317,10 @@ module ActiveRecord true end + def supports_partial_index? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -372,6 +391,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end @@ -455,14 +475,14 @@ module ActiveRecord # Escapes binary strings for bytea input to the database. def escape_bytea(value) - @connection.escape_bytea(value) if value + PGconn.escape_bytea(value) if value end # Unescapes bytea output from a database to the binary string it represents. # NOTE: This is NOT an inverse of escape_bytea! This is only to be used # on escaped binary output from database drive. def unescape_bytea(value) - @connection.unescape_bytea(value) if value + PGconn.unescape_bytea(value) if value end # Quotes PostgreSQL-specific data types for SQL input. @@ -470,6 +490,11 @@ module ActiveRecord return super unless column case value + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + else super + end when Float return super unless value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -501,6 +526,9 @@ module ActiveRecord when String return super unless 'bytea' == column.sql_type { :value => value, :format => 1 } + when Hash + return super unless 'hstore' == column.sql_type + PostgreSQLColumn.hstore_to_string(value) else super end @@ -696,12 +724,29 @@ module ActiveRecord Arel.sql("$#{index + 1}") end + class Result < ActiveRecord::Result + def initialize(columns, rows, column_types) + super(columns, rows) + @column_types = column_types + end + end + def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = binds.empty? ? exec_no_cache(sql, binds) : exec_cache(sql, binds) - ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = Result.new(result.fields, result.values, types) result.clear return ret end @@ -885,16 +930,20 @@ module ActiveRecord # add info on sort order for columns (only desc order is explicitly specified, asc is the default) desc_order_columns = inddef.scan(/(\w+) DESC/).flatten orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) end.compact end # Returns the list of all column definitions for a table. - def columns(table_name, name = nil) + def columns(table_name) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).collect do |column_name, type, default, notnull| - PostgreSQLColumn.new(column_name, default, type, notnull == 'f') + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') end end @@ -1151,6 +1200,22 @@ module ActiveRecord end private + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } + + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end + end + FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: def exec_no_cache(sql, binds) @@ -1280,7 +1345,7 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4e8932a695..962718da56 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -9,7 +9,7 @@ module ActiveRecord @tables = {} @columns = Hash.new do |h, table_name| - h[table_name] = conn.columns(table_name, "#{table_name} Columns") + h[table_name] = conn.columns(table_name) end @columns_hash = Hash.new do |h, table_name| diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 6391ea3800..55eca48efe 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -324,7 +324,7 @@ module ActiveRecord end # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name, name = nil) #:nodoc: + def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] when /^null$/i diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a2ce620354..c4a4c0ad9a 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -165,6 +165,7 @@ module ActiveRecord # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @columns_hash = self.class.column_types.dup init_internals @@ -190,6 +191,8 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = self.class.initialize_attributes(coder['attributes']) + @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) + init_internals @@ -209,6 +212,8 @@ module ActiveRecord # The dup method does not preserve the timestamps (created|updated)_(at|on). def initialize_dup(other) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + self.class.initialize_attributes(cloned_attributes) + cloned_attributes.delete(self.class.primary_key) @attributes = cloned_attributes @@ -243,7 +248,7 @@ module ActiveRecord # end # coder = {} # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } + # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) coder['attributes'] = attributes end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index fc76410499..1f8c4fc203 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -11,7 +11,10 @@ module ActiveRecord # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on # our own EXPLAINs now matter how loopingly beautiful that would be. - IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) + # + # On the other hand, we want to monitor the performance of our real database + # queries, not the performance of the access to the query cache. + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) def ignore_payload?(payload) payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index cf315b687c..b82d5b5621 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -3,7 +3,6 @@ require 'yaml' require 'zlib' require 'active_support/dependencies' require 'active_support/core_ext/object/blank' -require 'active_support/ordered_hash' require 'active_record/fixtures/file' if defined? ActiveRecord @@ -508,7 +507,7 @@ module ActiveRecord @name = fixture_name @class_name = class_name - @fixtures = ActiveSupport::OrderedHash.new + @fixtures = {} # Should be an AR::Base type class if class_name.is_a?(Class) diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index eaa7deac5a..2c766411a0 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -62,7 +62,7 @@ module ActiveRecord # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. - def instantiate(record) + def instantiate(record, column_types = {}) sti_class = find_sti_class(record[inheritance_column]) record_id = sti_class.primary_key && record[sti_class.primary_key] @@ -77,7 +77,9 @@ module ActiveRecord IdentityMap.add(instance) end else - instance = sti_class.allocate.init_with('attributes' => record) + column_types = sti_class.decorate_columns(column_types) + instance = sti_class.allocate.init_with('attributes' => record, + 'column_types' => column_types) end instance diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 61f82af0c3..b8764217d3 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -206,6 +206,26 @@ module ActiveRecord @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] end + def column_types # :nodoc: + @column_types ||= decorate_columns(columns_hash.dup) + end + + def decorate_columns(columns_hash) # :nodoc: + return if columns_hash.empty? + + serialized_attributes.keys.each do |key| + columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) + end + + columns_hash.each do |name, col| + if create_time_zone_conversion_attribute?(name, col) + columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col) + end + end + + columns_hash + end + # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults @@ -268,9 +288,16 @@ module ActiveRecord undefine_attribute_methods connection.schema_cache.clear_table_cache!(table_name) if table_exists? - @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil - @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil + @arel_engine = nil + @column_defaults = nil + @column_names = nil + @columns = nil + @columns_hash = nil + @column_types = nil + @content_columns = nil + @dynamic_methods_hash = nil + @inheritance_column = nil + @relation = nil end def clear_cache! # :nodoc: diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 09ee2ba61d..9bc046c775 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -287,6 +287,7 @@ module ActiveRecord IdentityMap.without do fresh_object = self.class.unscoped { self.class.find(id, options) } @attributes.update(fresh_object.instance_variable_get('@attributes')) + @columns_hash = fresh_object.instance_variable_get('@columns_hash') end @attributes_cache = {} diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 5945b05190..0e6fecbc4b 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/delegation' +require 'active_support/deprecation' module ActiveRecord module Querying @@ -36,7 +37,15 @@ module ActiveRecord def find_by_sql(sql, binds = []) logging_query_plan do result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - result_set.map { |record| instantiate(record) } + column_types = {} + + if result_set.respond_to? :column_types + column_types = result_set.column_types + else + ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" + end + + result_set.map { |record| instantiate(record, column_types) } end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f02f0544c5..8f8a3ec3bb 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -229,8 +229,8 @@ module ActiveRecord end end - def columns(tbl_name, log_msg) - @columns ||= klass.connection.columns(tbl_name, log_msg) + def columns(tbl_name) + @columns ||= klass.connection.columns(tbl_name) end def reset_column_information diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 6bf3050af9..63365e501b 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -177,8 +177,23 @@ module ActiveRecord # Person.where(:confirmed => true).limit(5).pluck(:id) # def pluck(column_name) - klass.connection.select_all(select(column_name).arel).map! do |attributes| - klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) + key = column_name.to_s.split('.', 2).last + + if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) + column_name = "#{table_name}.#{column_name}" + end + + result = klass.connection.select_all(select(column_name).arel) + types = result.column_types.merge klass.column_types + column = types[key] + + result.map do |attributes| + value = klass.initialize_attributes(attributes)[key] + if column + column.type_cast value + else + value + end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b6d762c2e2..87dd513880 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -206,8 +206,8 @@ module ActiveRecord # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. # - # This is useful in scenarios where you need a chainable response to a method - # or a scope that could return zero results. + # Used in cases where a method or scope could return zero records but the + # result needs to be chainable. # # For example: # diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 60a2e90e23..fb4b89b87b 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -8,12 +8,13 @@ module ActiveRecord class Result include Enumerable - attr_reader :columns, :rows + attr_reader :columns, :rows, :column_types def initialize(columns, rows) - @columns = columns - @rows = rows - @hash_rows = nil + @columns = columns + @rows = rows + @hash_rows = nil + @column_types = {} end def each diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2a565b51c6..dcbd165e58 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -197,6 +197,8 @@ HEADER index_orders = (index.orders || {}) statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + statement_parts << (':where => ' + index.where.inspect) if index.where + ' ' + statement_parts.join(', ') end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 257963c2ce..236ec563d2 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -4,6 +4,8 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base + attr_accessible :version + def self.table_name Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 7f1dba5095..2e60521638 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -162,8 +162,9 @@ module ActiveRecord #:nodoc: # # class IHaveMyOwnXML < ActiveRecord::Base # def to_xml(options = {}) + # require 'builder' # options[:indent] ||= 2 - # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) + # xml = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) # xml.instruct! unless options[:skip_instruct] # xml.level_one do # xml.tag!(:second_level, 'content') diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 8cc84f81d0..1c7b839e5e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,7 +15,7 @@ module ActiveRecord # class User < ActiveRecord::Base # store :settings, accessors: [ :color, :homepage ] # end - # + # # u = User.new(color: 'black', homepage: '37signals.com') # u.color # Accessor stored attribute # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor @@ -26,7 +26,7 @@ module ActiveRecord # end module Store extend ActiveSupport::Concern - + module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, Hash @@ -34,17 +34,19 @@ module ActiveRecord end def store_accessor(store_attribute, *keys) - Array(keys).flatten.each do |key| + keys.flatten.each do |key| define_method("#{key}=") do |value| + send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) send(store_attribute)[key] = value send("#{store_attribute}_will_change!") end - + define_method(key) do + send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) send(store_attribute)[key] end end end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 86687afdda..4d881f0f7d 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -14,7 +14,7 @@ module ActiveRecord end def teardown - ActiveRecord::SQLCounter.log.clear + SQLCounter.log.clear end def cleanup_identity_map @@ -30,5 +30,65 @@ module ActiveRecord assert_equal expected.to_s, actual.to_s, message end end + + def assert_sql(*patterns_to_match) + SQLCounter.log = [] + yield + SQLCounter.log + ensure + failed_patterns = [] + patterns_to_match.each do |pattern| + failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql } + end + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + end + + def assert_queries(num = 1) + SQLCounter.log = [] + yield + ensure + assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + end + + def assert_no_queries(&block) + prev_ignored_sql = SQLCounter.ignored_sql + SQLCounter.ignored_sql = [] + assert_queries(0, &block) + ensure + SQLCounter.ignored_sql = prev_ignored_sql + end + + end + + class SQLCounter + class << self + attr_accessor :ignored_sql, :log + end + + self.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/] + + # FIXME: this needs to be refactored so specific database can add their own + # ignored SQL. This ignored SQL is for Oracle. + ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + + + attr_reader :ignore + + def initialize(ignore = Regexp.union(self.class.ignored_sql)) + @ignore = ignore + end + + def call(name, start, finish, message_id, values) + sql = values[:sql] + + # FIXME: this seems bad. we should probably have a better way to indicate + # the query was cached + return if 'CACHE' == values[:name] || ignore =~ sql + self.class.log << sql + end end + + ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 7fd5d76ba5..9556878f63 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/prepend_and_append' + module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator @@ -49,7 +51,7 @@ module ActiveRecord class_hierarchy = [record.class] while class_hierarchy.first != @klass - class_hierarchy.insert(0, class_hierarchy.first.superclass) + class_hierarchy.prepend(class_hierarchy.first.superclass) end class_hierarchy.detect { |klass| !klass.abstract_class? } |