diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 67a66ad814..870d179592 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Allow escaping of literal colon characters in `sanitize_sql_*` methods when named bind variables are used + + *Justin Bull* + * Fix `#previously_new_record?` to return true for destroyed records. Before, if a record was created and then destroyed, `#previously_new_record?` would return true. diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 7ca5415c94..beb48174ed 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -137,7 +137,9 @@ def sanitize_sql_like(string, escape_character = "\\") end # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. + # sanitized and interpolated into the SQL statement. If using named bind + # variables in SQL statements where a colon is required verbatim use a + # backslash to escape. # # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) # # => "name='foo''bar' and group_id=4" @@ -145,6 +147,9 @@ def sanitize_sql_like(string, escape_character = "\\") # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # # => "name='foo''bar' and group_id=4" # + # sanitize_sql_array(["TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12\\:MI\\:SS')", date: "foo"]) + # # => "TO_TIMESTAMP('foo', 'YYYY/MM/DD HH12:MI:SS')" + # # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) # # => "name='foo''bar' and group_id='4'" # @@ -206,9 +211,11 @@ def replace_bind_variable(value, c = connection) end def replace_named_bind_variables(statement, bind_vars) - statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| + statement.gsub(/([:\\]?):([a-zA-Z]\w*)/) do |match| if $1 == ":" # skip postgresql casts match # return the whole match + elsif $1 == "\\" # escaped literal colon + match[1..-1] # return match with escaping backlash char removed elsif bind_vars.include?(match = $2.to_sym) replace_bind_variable(bind_vars[match]) else diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index cb32141c9f..813d868b6d 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -224,6 +224,11 @@ def test_named_bind_with_postgresql_type_casts assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end + def test_named_bind_with_literal_colons + assert_equal "TO_TIMESTAMP('2017/08/02 10:59:00', 'YYYY/MM/DD HH12:MI:SS')", bind("TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12\\:MI\\:SS')", date: "2017/08/02 10:59:00") + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12:MI:SS')", date: "2017/08/02 10:59:00" } + end + private def bind(statement, *vars) if vars.first.is_a?(Hash)