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:
parent
bf43b4c33f
commit
0492ea6d39
6 changed files with 88 additions and 14 deletions
|
@ -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`.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -104,27 +104,59 @@ 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)
|
||||
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)
|
||||
send :"#{store_attribute}=", attribute
|
||||
object.send :"#{store_attribute}=", attribute
|
||||
end
|
||||
attribute
|
||||
end
|
||||
end
|
||||
|
||||
class IndifferentCoder # :nodoc:
|
||||
def initialize(coder_or_class_name)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue