Find_by_sql not quoting properly (in acts_as_taggable plugin

I have run into a very strange problem discovered through the use of
the acts_as_taggable plugin, but related to quoting/sanitizing the
interpolated list in a find_by_sql. Apologies for the length, but I
wanted to be complete. :wink:

The method from acts_as_taggable.rb is:

     def find_tagged_with(list)
       find_by_sql(["SELECT #{table_name}.* FROM #{table_name},

tags, taggings " +
"WHERE #{table_name}.#{primary_key} =
taggings.taggable_id " +
"AND taggings.taggable_type = ‘#
{acts_as_taggable_options[:taggable_type]}’ " +
“AND taggings.tag_id = tags.id AND tags.name
IN (?)”, list]
)
end

Originally, this built a string directly to pass to find_by_sql, but
I changed it thusly:

trunk/vendor/plugins/acts_as_taggable/lib/acts_as_taggable.rb
r13 r127
25 25 module SingletonMethods
26 26 def find_tagged_with(list)
27 find_by_sql(
28 "SELECT #{table_name}.* FROM #{table_name}, tags,
taggings " +
29 "WHERE #{table_name}.#{primary_key} =
taggings.taggable_id " +
30 "AND taggings.taggable_type = ‘#
{acts_as_taggable_options[:taggable_type]}’ " +
31 “AND taggings.tag_id = tags.id AND tags.name IN (#
{list.collect { |name| “‘#{name}’” }.join(”, “)})”
27 find_by_sql(["SELECT #{table_name}.* FROM #
{table_name}, tags, taggings " +
28 "WHERE #{table_name}.#{primary_key} =
taggings.taggable_id " +
29 "AND taggings.taggable_type = ‘#
{acts_as_taggable_options[:taggable_type]}’ " +
30 “AND taggings.tag_id = tags.id AND
tags.name IN (?)”, list]
32 31 )
33 32 end

As you can see, if list.any? {|t| t.include?(“'”)} the resulting
string could have a bad set of single quotes.

And, of course, on my development machine it fixed the problem
wonderfully:

$ uname -mpsrv ; mysql --version ; rails --version
Darwin 8.7.1 Darwin Kernel Version 8.7.1: Wed Jun 7 16:19:56 PDT
2006; root:xnu-792.9.72.obj~2/RELEASE_I386 i386 i386
mysql Ver 14.7 Distrib 4.1.18, for apple-darwin8.3.1 (i686) using
readline 4.3
Rails 1.1.2

And rails is frozen:
rab:trunk $ svn info vendor/rails/
Path: vendor/rails
URL: http://dev.rubyonrails.org/svn/rails/tags/rel_1-1-2
Repository Root: http://dev.rubyonrails.org/svn/rails
Repository UUID: 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Revision: 4515
Node Kind: directory
Schedule: normal
Last Changed Author: david
Last Changed Rev: 4205
Last Changed Date: 2006-04-09 18:15:39 -0400 (Sun, 09 Apr 2006)
Properties Last Updated: 2006-06-29 21:21:10 -0400 (Thu, 29 Jun 2006)

Now we move to production and expect the same behavior – after all,
the platforms are similar, right?

$ uname -mpsrv ; mysql --version ; rails --version
FreeBSD 5.4-STABLE FreeBSD 5.4-STABLE #2: Tue Sep 6 04:42:26 GMT
2005 [email protected]:/usr/obj/usr/src/sys/TEXTDRIVEN
i386 i386
mysql Ver 14.7 Distrib 4.1.18, for portbld-freebsd5.4 (i386) using 4.3
Rails 1.1.2

However, these are the snippets from the development.log on my Mac
and the production.log on TextDrive:

Processing TagController#index (for 127.0.0.1 at 2006-06-30 10:46:46)
[GET]
Session ID: 63f13e4e972204fd2e0021949cb25901
Parameters: {“action”=>“index”, “id”=>“don’t”, “controller”=>“tag”}
Tag Columns (0.000270) SHOW FIELDS FROM tags
Tag Load (0.000681) SELECT * FROM tags WHERE (tags.name = ‘don
't’ ) LIMIT 1
Post Load (0.011631) SELECT posts.* FROM posts, tags, taggings
WHERE posts.id = taggings.taggable_id AND taggings.taggable_type
= ‘Post’ AND taggings.tag_id = tags.id AND tags.name IN (‘don't’)
Post Columns (0.000269) SHOW FIELDS FROM posts
Rendering within layouts/application
Rendering tag/index

Note the “IN (‘don't’)” in the SQL, but in production.log:

Processing TagController#index (for xxx.xxx.xxx.xxx at 2006-06-30
14:54:57) [GET]
Session ID: d07b7fa700bb02bdffb15639505200b6
Parameters: {“action”=>“index”, “id”=>“don’t”, “controller”=>“tag”}

ActiveRecord::StatementInvalid (Mysql::Error: You have an error in
your SQL syntax; check the manual that corresponds to your MySQL
server version for the right syntax to use near ‘t’)’ at line 1:
SELECT posts.* FROM posts, tags, taggings WHERE posts.id = taggings
.taggable_id AND taggings.taggable_type = ‘Post’ AND taggings.tag_id
= tags.id AND tags.name IN (‘don’t’)):
/vendor/rails/activerecord/lib/active_record/connection_adapters/
abstract_adapter.rb:120:in log' /vendor/rails/activerecord/lib/active_record/connection_adapters/ mysql_adapter.rb:185:in execute’
/vendor/rails/activerecord/lib/active_record/connection_adapters/
mysql_adapter.rb:337:in select' /vendor/rails/activerecord/lib/active_record/connection_adapters/ mysql_adapter.rb:176:in select_all’
/vendor/rails/activerecord/lib/active_record/base.rb:390:in
find_by_sql' /vendor/plugins/acts_as_taggable/lib/acts_as_taggable.rb:31:in find_tagged_with’
/app/controllers/tag_controller.rb:15:in index' /vendor/rails/actionpack/lib/action_controller/base.rb:910:in perform_action_without_filters’
/vendor/rails/actionpack/lib/action_controller/filters.rb:368:in
perform_action_without_benchmark' /vendor/rails/actionpack/lib/action_controller/benchmarking.rb: 69:in perform_action_without_rescue’
/usr/local/lib/ruby/1.8/benchmark.rb:293:in measure' /vendor/rails/actionpack/lib/action_controller/benchmarking.rb: 69:in perform_action_without_rescue’
/vendor/rails/actionpack/lib/action_controller/rescue.rb:82:in
perform_action' /vendor/rails/actionpack/lib/action_controller/base.rb:381:in process_without_filters’
/vendor/rails/actionpack/lib/action_controller/filters.rb:377:in
process_without_session_management_support' /vendor/rails/actionpack/lib/action_controller/ session_management.rb:117:in process’
/vendor/rails/railties/lib/dispatcher.rb:38:in dispatch' /vendor/rails/railties/lib/fcgi_handler.rb:150:in process_request’
/vendor/rails/railties/lib/fcgi_handler.rb:54:in process!' /usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/./fcgi.rb:600:in each_cgi’
/usr/local/lib/ruby/gems/1.8/gems/fcgi-0.8.6.1/./fcgi.rb:597:in
each_cgi' /vendor/rails/railties/lib/fcgi_handler.rb:53:in process!’
/vendor/rails/railties/lib/fcgi_handler.rb:23:in `process!’
/users/home/xxxxx-xxxxx/xxxxx/xxxxx.xxxxx/web/public/
dispatch.fcgi:24

exception occured: Mysql::Error: You have an error in your SQL
syntax; check the manual that corresponds to your MySQL server versio
n for the right syntax to use near ‘t’)’ at line 1: SELECT posts.*
FROM posts, tags, taggings WHERE posts.id = taggings.taggable_id
AND taggings.taggable_type = ‘Post’ AND taggings.tag_id = tags.id AND
tags.name IN (‘don’t’)
Rendering status500layoutfalsefile/users/home/xxxxx-xxxxx/xxxxx/
xxxxx.xxxxx/web/public/…/config/…/public/500.html within layouts/
application
Rendering /users/home/xxxxx-xxxxx/xxxxx/xxxxx.xxxxx/web/public/…/
config/…/public/500.html (500)

BAM! See the “IN (‘don’t’)”, I know that’s bad, but all the code is
the same so why isn’t it being quoted properly??

I’ve trolled down into active_record/base.rb and the call tree: (dive
in yourself if you prefer…)
find_by_sql(sql)
sanitize_sql(sql)
statement, *values = sql
replace_bind_variables(statement, values)
bound = values.dup
statement.gsub(‘?’) { quote_bound_value(bound.shift) }
if (value.respond_to?
(:map) && !value.is_a?(String))
value.map { |v|
connection.quote(v) }.join(‘,’)
else
connection.quote(value)
end
looks like the tag or tags in list will be quoted whether “don’t” or
[“don’t”] (with the same result, too).

Does this make sense to anyone? If it worked the same on both
systems, I’d think that there was an extra interpolation of the sql
that turns “'” into “'” (thus undoing the sanitize_sql and quote work).

-Rob

Rob B. http://agileconsultingllc.com
[email protected]
+1 513-295-4739