Allow use of backslashes to escape literal colons

Despite the inconvenience of double-backslashing, the backslash was chosen
because it's a very common char to use in escaping across multiple programming
languages. A developer, without looking at documentation, may intuitively try
to use it to achieve the desired results in this scenario.

Fixes #37779
This commit is contained in:
Justin Bull 2019-11-25 11:51:39 -05:00
parent c78432b33f
commit 694376f15e
3 changed files with 18 additions and 2 deletions

@ -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.

@ -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

@ -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)