1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

ActiveRecord::Store works together with PG hstore columns.

This is necessary because as of 5ac2341 `hstore` columns are always stored
as `Hash` with `String` keys. `ActiveRecord::Store` expected the attribute to
be an instance of `HashWithIndifferentAccess`, which led to the bug.
This commit is contained in:
Yves Senn 2013-10-10 14:41:14 +02:00
parent bf43b4c33f
commit 0492ea6d39
6 changed files with 88 additions and 14 deletions

View file

@ -1,3 +1,8 @@
* `ActiveRecord::Store` works together with PG `hstore` columns.
Fixes #12452.
*Yves Senn*
* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of
all registered `stored_attributes`. Now every subclass of
`ActiveRecord::Base` has it's own `Hash`.

View file

@ -66,6 +66,10 @@ module ActiveRecord
def type
@column.type
end
def accessor
ActiveRecord::Store::IndifferentHashAccessor
end
end
class Attribute < Struct.new(:coder, :value, :state) # :nodoc:

View file

@ -234,6 +234,10 @@ module ActiveRecord
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
end
class Cidr < Type
@ -250,6 +254,10 @@ module ActiveRecord
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
end
class TypeMap

View file

@ -148,6 +148,10 @@ module ActiveRecord
@oid_type.type_cast value
end
def accessor
@oid_type.accessor
end
private
def has_default_function?(default_value, default)

View file

@ -104,26 +104,58 @@ module ActiveRecord
protected
def read_store_attribute(store_attribute, key)
attribute = initialize_store_attribute(store_attribute)
attribute[key]
accessor = store_accessor_for(store_attribute)
accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
attribute = initialize_store_attribute(store_attribute)
if value != attribute[key]
send :"#{store_attribute}_will_change!"
attribute[key] = value
end
accessor = store_accessor_for(store_attribute)
accessor.write(self, store_attribute, key, value)
end
private
def initialize_store_attribute(store_attribute)
attribute = send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
attribute = IndifferentCoder.as_indifferent_hash(attribute)
send :"#{store_attribute}=", attribute
def store_accessor_for(store_attribute)
@column_types[store_attribute.to_s].accessor
end
class HashAccessor
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
end
def self.write(object, attribute, key, value)
prepare(object, attribute)
if value != read(object, attribute, key)
object.public_send :"#{attribute}_will_change!"
object.public_send(attribute)[key] = value
end
end
def self.prepare(object, attribute)
object.public_send :"#{attribute}=", {} unless object.send(attribute)
end
end
class StringKeyedHashAccessor < HashAccessor
def self.read(object, attribute, key)
super object, attribute, key.to_s
end
def self.write(object, attribute, key, value)
super object, attribute, key.to_s, value
end
end
class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
attribute = IndifferentCoder.as_indifferent_hash(attribute)
object.send :"#{store_attribute}=", attribute
end
attribute
end
attribute
end
class IndifferentCoder # :nodoc:
@ -141,7 +173,7 @@ module ActiveRecord
end
def load(yaml)
self.class.as_indifferent_hash @coder.load(yaml)
self.class.as_indifferent_hash(@coder.load(yaml))
end
def self.as_indifferent_hash(obj)

View file

@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlHstoreTest < ActiveRecord::TestCase
class Hstore < ActiveRecord::Base
self.table_name = 'hstores'
store_accessor :settings, :language, :timezone
end
def setup
@ -26,6 +28,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('hstores') do |t|
t.hstore 'tags', :default => ''
t.hstore 'settings'
end
end
@column = Hstore.columns.find { |c| c.name == 'tags' }
@ -90,6 +93,24 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
end
def test_with_store_accessors
x = Hstore.new(language: "fr", timezone: "GMT")
assert_equal "fr", x.language
assert_equal "GMT", x.timezone
x.save!
x = Hstore.first
assert_equal "fr", x.language
assert_equal "GMT", x.timezone
x.language = "de"
x.save!
x = Hstore.first
assert_equal "de", x.language
assert_equal "GMT", x.timezone
end
def test_gen1
assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
end