Skip to content

Commit

Permalink
Resolve encoding issues with arrays of hstore (bug 11135).
Browse files Browse the repository at this point in the history
We didn't have enough encoding for the wire protocol to store an array
of hstore types. So, further encode any hstore that is an array member.
Whilst we're here, ensure it's an HashWithIndifferentAccess being
returned, to be consistent with other serialized forms, and add testing
for arrays of hstore.

So now the following migration:

  enable_extension "hstore"
  create_table :servers do |t|
    t.string  :name
    t.hstore  :interfaces, array: true
  end

produces a model that can used like this, to store an array of hashes:

  server = Server.create(name: "server01", interfaces: [
    { name: "bge0", ipv4: "192.0.2.2", state: "up" },
    { name: "de0", state: "disabled", by: "misha" },
    { name: "fe0", state: "up" },
  ])

More at http://inopinatus.org/2013/07/12/using-arrays-of-hstore-with-rails-4/
  • Loading branch information
inopinatus authored and gsamokovarov committed Feb 16, 2014
1 parent 3e3ed1e commit da3fec2
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 7 deletions.
6 changes: 6 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
* Perform necessary deeper encoding when hstore is inside an array.

Fixes #11135.

*Josh Goodall*, *Genadi Samokovarov*

* Properly detect if a connection is still active before using it
in multi-threaded environments.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ def string_to_bit(value)
end
end

def hstore_to_string(object)
def hstore_to_string(object, array_member = false)
if Hash === object
object.map { |k,v|
"#{escape_hstore(k)}=>#{escape_hstore(v)}"
}.join ','
string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',')
string = escape_hstore(string) if array_member
string
else
object
end
Expand All @@ -49,10 +49,10 @@ def string_to_hstore(string)
if string.nil?
nil
elsif String === string
Hash[string.scan(HstorePair).map { |k,v|
Hash[string.scan(HstorePair).map { |k, v|
v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
[k,v]
[k, v]
}]
else
string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def type_cast(value, column, array_member = false)
end
when Hash
case column.sql_type
when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
when 'json' then PostgreSQLColumn.json_to_string(value)
else super(value, column)
end
Expand Down
39 changes: 39 additions & 0 deletions activerecord/test/cases/adapters/postgresql/hstore_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def setup
@connection.transaction do
@connection.create_table('hstores') do |t|
t.hstore 'tags', :default => ''
t.hstore 'payload', array: true
t.hstore 'settings'
end
end
Expand Down Expand Up @@ -182,6 +183,30 @@ def test_select
assert_equal({'1' => '2'}, x.tags)
end

def test_array_cycle
assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
end

def test_array_strings_with_quotes
assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
end

def test_array_strings_with_commas
assert_array_cycle([{'this,has' => 'many,values'}])
end

def test_array_strings_with_array_delimiters
assert_array_cycle(['{' => '}'])
end

def test_array_strings_with_null_strings
assert_array_cycle([{'NULL' => 'NULL'}])
end

def test_contains_nils
assert_array_cycle([{'NULL' => nil}])
end

def test_select_multikey
@connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
x = Hstore.first
Expand Down Expand Up @@ -237,6 +262,20 @@ def test_update_all

private

def assert_array_cycle(array)
# test creation
x = Hstore.create!(payload: array)
x.reload
assert_equal(array, x.payload)

# test updating
x = Hstore.create!(payload: [])
x.payload = array
x.save!
x.reload
assert_equal(array, x.payload)
end

def assert_cycle(hash)
# test creation
x = Hstore.create!(:tags => hash)
Expand Down

0 comments on commit da3fec2

Please sign in to comment.