From e6ae6a22f4a3f0fa1137a99b8a38a410459b42a1 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 1 Dec 2011 10:04:27 -0800 Subject: [PATCH] Handle usage of on_duplicate_key_update in MySQL prepared statements (Fixes #404) This fixes a general class of bug where columns was called by the dataset literalization code when using prepared statements. The columns call would call the prepared statement literalization to recurse, usually leading to a SystemStackError or a NoMemoryError. The fix is fairly simple. Prepared statements now have a link to the dataset that created them, and calling columns on a prepared statement is now delegated to the dataset that prepared it. --- lib/sequel/dataset/prepared_statements.rb | 10 ++++++++++ spec/adapters/mysql_spec.rb | 10 ++++++++++ spec/core/dataset_spec.rb | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/lib/sequel/dataset/prepared_statements.rb b/lib/sequel/dataset/prepared_statements.rb index c063a0fd96..f51555d013 100644 --- a/lib/sequel/dataset/prepared_statements.rb +++ b/lib/sequel/dataset/prepared_statements.rb @@ -63,6 +63,9 @@ module PreparedStatementMethods # The array/hash of bound variable placeholder names. attr_accessor :prepared_args + # The dataset that created this prepared statement. + attr_accessor :orig_dataset + # The argument to supply to insert and update, which may use # placeholders specified by prepared_args attr_accessor :prepared_modify_values @@ -72,6 +75,12 @@ module PreparedStatementMethods def call(bind_vars={}, &block) bind(bind_vars).run(&block) end + + # Send the columns to the original dataset, as calling it + # on the prepared statement can cause problems. + def columns + orig_dataset.columns + end # Returns the SQL for the prepared statement, depending on # the type of the statement and the prepared_modify_values. @@ -244,6 +253,7 @@ def prepare(type, name=nil, *values) def to_prepared_statement(type, values=nil) ps = bind ps.extend(PreparedStatementMethods) + ps.orig_dataset = self ps.prepared_type = type ps.prepared_modify_values = values ps diff --git a/spec/adapters/mysql_spec.rb b/spec/adapters/mysql_spec.rb index b8fde58a57..dab071c794 100644 --- a/spec/adapters/mysql_spec.rb +++ b/spec/adapters/mysql_spec.rb @@ -247,6 +247,16 @@ def logger.method_missing(m, msg) @d.first[:name].should == ':\\' end + + specify "should handle prepared statements with on_duplicate_key_update" do + @d.db.add_index :items, :value, :unique=>true + ds = @d.on_duplicate_key_update + ps = ds.prepare(:insert, :insert_user_id_feature_name, :value => :$v, :name => :$n) + ps.call(:v => 1, :n => 'a') + ds.all.should == [{:value=>1, :name=>'a'}] + ps.call(:v => 1, :n => 'b') + ds.all.should == [{:value=>1, :name=>'b'}] + end end describe "MySQL datasets" do diff --git a/spec/core/dataset_spec.rb b/spec/core/dataset_spec.rb index d71439880f..66d75f1c6e 100644 --- a/spec/core/dataset_spec.rb +++ b/spec/core/dataset_spec.rb @@ -3066,6 +3066,13 @@ class ::InspectDataset < Sequel::Dataset; end @db.sqls.should == ['SELECT * FROM items WHERE (num = 1)'] end + specify "should handle columns on prepared statements correctly" do + @db.columns = [:num] + @ds.meta_def(:select_where_sql){|sql| super(sql); sql << " OR #{columns.first} = 1" if opts[:where]} + @ds.filter(:num=>:$n).prepare(:select, :sn).sql.should == 'SELECT * FROM items WHERE (num = $n) OR num = 1' + @db.sqls.should == ['SELECT * FROM items LIMIT 1'] + end + specify "should handle datasets using static sql and placeholders" do @db["SELECT * FROM items WHERE (num = ?)", :$n].call(:select, :n=>1) @db.sqls.should == ['SELECT * FROM items WHERE (num = 1)']