aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb40
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb14
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb85
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb243
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb366
-rw-r--r--activerecord/lib/active_record/core.rb3
-rw-r--r--activerecord/lib/active_record/inheritance.rb6
-rw-r--r--activerecord/lib/active_record/model_schema.rb33
-rw-r--r--activerecord/lib/active_record/persistence.rb1
-rw-r--r--activerecord/lib/active_record/querying.rb11
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb16
-rw-r--r--activerecord/lib/active_record/result.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb76
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb6
-rw-r--r--activerecord/test/cases/base_test.rb30
-rw-r--r--activerecord/test/cases/calculations_test.rb7
-rw-r--r--activerecord/test/cases/column_definition_test.rb9
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb7
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb11
21 files changed, 749 insertions, 237 deletions
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 7efef73472..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
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 6b0230384b..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
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 34d88edff3..78e54c4c9b 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -66,6 +66,10 @@ 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?
@@ -98,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
@@ -116,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/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 f1940ea15f..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'
@@ -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.
@@ -376,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
@@ -474,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}'"
@@ -505,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
@@ -700,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
@@ -898,8 +939,11 @@ module ActiveRecord
# Returns the list of all column definitions for a table.
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
@@ -1156,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)
@@ -1285,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/core.rb b/activerecord/lib/active_record/core.rb
index 5b88b26310..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
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/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 50239f7cb2..63365e501b 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -177,11 +177,23 @@ module ActiveRecord
# Person.where(:confirmed => true).limit(5).pluck(:id)
#
def pluck(column_name)
+ 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
- klass.connection.select_all(select(column_name).arel).map! do |attributes|
- klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes))
+
+ 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/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/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 33bf4478cc..1644a58d92 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlHstoreTest < ActiveRecord::TestCase
class Hstore < ActiveRecord::Base
@@ -10,12 +12,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('hstores') do |t|
- t.hstore 'tags'
+ t.hstore 'tags', :default => ''
end
end
rescue ActiveRecord::StatementInvalid
return skip "do not test on PG without hstore"
end
+ @column = Hstore.columns.find { |c| c.name == 'tags' }
end
def teardown
@@ -23,21 +26,74 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
end
def test_column
- column = Hstore.columns.find { |c| c.name == 'tags' }
- assert column
- assert_equal :hstore, column.type
+ assert_equal :hstore, @column.type
end
def test_type_cast_hstore
- column = Hstore.columns.find { |c| c.name == 'tags' }
- assert column
+ assert @column
data = "\"1\"=>\"2\""
- hash = column.class.cast_hstore data
+ hash = @column.class.string_to_hstore data
assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, column.type_cast(data))
+ assert_equal({'1' => '2'}, @column.type_cast(data))
+
+ assert_equal({}, @column.type_cast(""))
+ assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
+
+ def test_gen1
+ assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ end
+
+ def test_gen2
+ assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ end
+
+ def test_gen3
+ assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ end
+
+ def test_gen4
+ assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ end
+
+ def test_parse1
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
+
+ def test_parse2
+ assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
+ end
+
+ def test_parse3
+ assert_equal({"=" => ">"}, @column.type_cast("==>>"))
end
+ def test_parse4
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
+ end
+
+ def test_parse5
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
+ end
+
+ def test_parse6
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
+ end
+
+ def test_parse7
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
+ end
+
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.find :first
+ x.tags = { '"a\'' => 'b' }
+ assert x.save!
+ end
+
+
def test_select
@connection.execute "insert into hstores (tags) VALUES ('1=>2')"
x = Hstore.find :first
@@ -54,6 +110,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle('a' => 'b', '1' => '2')
end
+ def test_nil
+ assert_cycle('a' => nil)
+ end
+
def test_quotes
assert_cycle('a' => 'b"ar', '1"foo' => '2')
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index d6de668a17..3ac2a76b96 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -772,11 +772,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
private
def cached_columns
- @cached_columns ||= time_related_columns_on_topic.map(&:name)
+ Topic.columns.find_all { |column|
+ !Topic.serialized_attributes.include? column.name
+ }.map(&:name)
end
def time_related_columns_on_topic
- Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) }
+ Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
end
def in_time_zone(zone)
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index dfa05990f9..698c3d0cb1 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1150,7 +1150,8 @@ class BasicsTest < ActiveRecord::TestCase
# use a geometric function to test for an open path
objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id]
- assert_equal objs[0].isopen, 't'
+
+ assert_equal true, objs[0].isopen
# test alternate formats when defining the geometric types
@@ -1178,7 +1179,8 @@ class BasicsTest < ActiveRecord::TestCase
# use a geometric function to test for an closed path
objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
- assert_equal objs[0].isclosed, 't'
+
+ assert_equal true, objs[0].isclosed
end
end
@@ -1974,4 +1976,28 @@ class BasicsTest < ActiveRecord::TestCase
def test_table_name_with_2_abstract_subclasses
assert_equal "photos", Photo.table_name
end
+
+ def test_column_types_typecast
+ topic = Topic.first
+ refute_equal 't.lo', topic.author_name
+
+ attrs = topic.attributes.dup
+ attrs.delete 'id'
+
+ typecast = Class.new {
+ def type_cast value
+ "t.lo"
+ end
+ }
+
+ types = { 'author_name' => typecast.new }
+ topic = Topic.allocate.init_with 'attributes' => attrs,
+ 'column_types' => types
+
+ assert_equal 't.lo', topic.author_name
+ end
+
+ def test_typecasting_aliases
+ assert_equal 10, Topic.select('10 as tenderlove').first.tenderlove
+ end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index e1544b3b00..0391319a00 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -486,11 +486,6 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_not_auto_table_name_prefix_if_column_joined
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # FIXME: PostgreSQL should works in the same way of the other adapters
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal ["7"], Company.joins(:contracts).pluck(:developer_id)
- else
- assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
- end
+ assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 14884e42af..a44b49466f 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -126,17 +126,20 @@ module ActiveRecord
if current_adapter?(:PostgreSQLAdapter)
def test_bigint_column_should_map_to_integer
- bigint_column = PostgreSQLColumn.new('number', nil, "bigint")
+ oid = PostgreSQLAdapter::OID::Identity.new
+ bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint")
assert_equal :integer, bigint_column.type
end
def test_smallint_column_should_map_to_integer
- smallint_column = PostgreSQLColumn.new('number', nil, "smallint")
+ oid = PostgreSQLAdapter::OID::Identity.new
+ smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint")
assert_equal :integer, smallint_column.type
end
def test_uuid_column_should_map_to_string
- uuid_column = PostgreSQLColumn.new('unique_id', nil, "uuid")
+ oid = PostgreSQLAdapter::OID::Identity.new
+ uuid_column = PostgreSQLColumn.new('unique_id', nil, oid, "uuid")
assert_equal :string, uuid_column.type
end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index c79382a984..4de0999dbb 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -238,6 +238,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_tsvector_shorthand_definition
output = standard_dump
+ if %r{create_table "postgresql_hstores"} =~ output
+ assert_match %r{t.hstore "hash_store", default => ""}, output
+ end
+ end
+
+ def test_schema_dump_includes_tsvector_shorthand_definition
+ output = standard_dump
if %r{create_table "postgresql_tsvectors"} =~ output
assert_match %r{t.tsvector "text_vector"}, output
end
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 5cf9a207f3..25b416a906 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,6 +1,6 @@
ActiveRecord::Schema.define do
- %w(postgresql_tsvectors postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings
+ %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -63,6 +63,15 @@ _SQL
);
_SQL
+ if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)")
+ execute <<_SQL
+ CREATE TABLE postgresql_hstores (
+ id SERIAL PRIMARY KEY,
+ hash_store hstore default ''::hstore
+ );
+_SQL
+ end
+
execute <<_SQL
CREATE TABLE postgresql_moneys (
id SERIAL PRIMARY KEY,