diff --git a/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb b/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb index 1da3d9f..1a22d55 100644 --- a/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb +++ b/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb @@ -259,8 +259,10 @@ def type_cast_with_extended_types(value, column, part_array = false) alias_method_chain :type_cast, :extended_types def quote_with_extended_types(value, column = nil) - if [Array, IPAddr].include? value.class + if value.is_a? IPAddr "'#{type_cast(value, column)}'" + elsif value.is_a? Array + "'#{array_to_string(value, column, true)}'" else quote_without_extended_types(value, column) end @@ -319,15 +321,28 @@ def ipaddr_to_string(value) "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" end - def array_to_string(value, column) - "{#{value.map { |val| item_to_string(val, column) }.join(',')}}" + def array_to_string(value, column, encode_single_quotes = false) + "{#{value.map { |val| item_to_string(val, column, encode_single_quotes) }.join(',')}}" end - def item_to_string(value, column) + def item_to_string(value, column, encode_single_quotes = false) if value.nil? 'NULL' - elsif value.is_a?String - '"' + type_cast(value, column, true).gsub('"', '\\"') + '"' + elsif value.is_a? String + value = type_cast(value, column, true).dup + # Encode backslashes. One backslash becomes 4 in the resulting SQL. + # (why 4, and not 2? Trial and error shows 4 works, 2 fails to parse.) + value.gsub!('\\', '\\\\\\\\') + # Encode a bare " in the string as \" + value.gsub!('"', '\\"') + # PostgreSQL parses the string values differently if they are quoted for + # use in a statement, or if it will be used as part of a bound argument. + # For directly-inserted values (UPDATE foo SET bar='{"array"}') we need to + # escape ' as ''. For bound arguments, do not escape them. + if encode_single_quotes + value.gsub!("'", "''") + end + "\"#{value}\"" else type_cast(value, column, true) end diff --git a/lib/postgres_ext/arel/visitors/to_sql.rb b/lib/postgres_ext/arel/visitors/to_sql.rb index 00a14a6..05ef681 100644 --- a/lib/postgres_ext/arel/visitors/to_sql.rb +++ b/lib/postgres_ext/arel/visitors/to_sql.rb @@ -22,11 +22,7 @@ def visit_IPAddr value def change_string value return value unless value.is_a?(String) - if value.match /"|,|\{/ - value.gsub(/"/, "\"").gsub(/'/,'"') - else - value.gsub(/'/,'') - end + value.gsub(/^\'/, '"').gsub(/\'$/, '"') end end end diff --git a/spec/arel/array_spec.rb b/spec/arel/array_spec.rb index 8eb7c12..fcec216 100644 --- a/spec/arel/array_spec.rb +++ b/spec/arel/array_spec.rb @@ -23,7 +23,7 @@ class ArelArray < ActiveRecord::Base it 'converts Arel array_overlap statment' do arel_table = ArelArray.arel_table - arel_table.where(arel_table[:tags].array_overlap(['tag','tag 2'])).to_sql.should match /&& '\{tag,tag 2\}'/ + arel_table.where(arel_table[:tags].array_overlap(['tag','tag 2'])).to_sql.should match /&& '\{"tag","tag 2"\}'/ end it 'converts Arel array_overlap statment' do diff --git a/spec/models/array_spec.rb b/spec/models/array_spec.rb index c9ea403..f4841e2 100644 --- a/spec/models/array_spec.rb +++ b/spec/models/array_spec.rb @@ -87,6 +87,105 @@ class User < ActiveRecord::Base end end end + + describe 'strings contain special characters' do + context '#save' do + it 'contains: \'' do + data = ['some\'thing'] + u = User.create + u.nick_names = data + u.save! + u.reload + u.nick_names.should eq data + end + + it 'contains: {' do + data = ['some{thing'] + u = User.create + u.nick_names = data + u.save! + u.reload + u.nick_names.should eq data + end + + it 'contains: }' do + data = ['some}thing'] + u = User.create + u.nick_names = data + u.save! + u.reload + u.nick_names.should eq data + end + + it 'contains: backslash' do + data = ['some\\thing'] + u = User.create + u.nick_names = data + u.save! + u.reload + u.nick_names.should eq data + end + + it 'contains: "' do + data = ['some"thing'] + u = User.create + u.nick_names = data + u.save! + u.reload + u.nick_names.should eq data + end + end + + context '#create' do + it 'contains: \'' do + data = ['some\'thing'] + u = User.create(:nick_names => data) + u.reload + u.nick_names.should eq data + end + + it 'contains: {' do + data = ['some{thing'] + u = User.create(:nick_names => data) + u.reload + u.nick_names.should eq data + end + + it 'contains: }' do + data = ['some}thing'] + u = User.create(:nick_names => data) + u.reload + u.nick_names.should eq data + end + + it 'contains: backslash' do + data = ['some\\thing'] + u = User.create(:nick_names => data) + u.reload + u.nick_names.should eq data + end + + it 'contains: "' do + data = ['some"thing'] + u = User.create(:nick_names => data) + u.reload + u.nick_names.should eq data + end + end + end + + describe 'array_overlap' do + it "works" do + arel = User.arel_table + User.create(:nick_names => ['this']) + x = User.create + x.nick_names = ["s'o{m}e", 'thing'] + x.save + u = User.where(arel[:nick_names].array_overlap(["s'o{m}e"])) + u.first.should_not be_nil + u.first.nick_names.should eq ["s'o{m}e", 'thing'] + end + end end context 'default values' do