From f15c1f9e00d39e64d57254870560340b0186d479 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Wed, 9 Sep 2009 14:17:10 -0700 Subject: [PATCH] Allow splitting of multiple result sets into separate arrays when using multiple statements in a single query in the native MySQL adapter This allows you to use Dataset#split_multiple_result_sets when using the native MySQL adapter to have each yield arrays of rows, one per statement, instead of yielding hashes for all result sets. This makes it possible to determine which statements yielded which result sets. Because of the way Sequel is architected, you cannot graph a dataset that splits multiple statements. There are no plans to rearchitect Sequel to accomodate that. Having each yield arrays of hashes instead of hashes breaks numerous internal assumptions held by Sequel. For example, row_proc assigned to split_multiple_result_sets datasets have to be recoded to accept arrays of hashes instead of plain hashes. Note that when you split a multiple result set dataset, it will modify an existing row_proc for you, so this particular issue won't be a problem. However, you should be aware that having each returning arrays instead of hash may cause unexpected breakage elsewhere. While here, fix another commands out of sync bug that occured when multiple statements. Before, if you executed a multiple result set query and returned before getting all of the results (say by using Dataset#first), the next query would get a commands out of sync message. Now, an ensure block inside of Database#execute will get all remaining result sets so that should no longer happen. This was found while writing tests for the split multiple result set code. --- CHANGELOG | 4 +++ lib/sequel/adapters/mysql.rb | 59 +++++++++++++++++++++++++++++++----- spec/adapters/mysql_spec.rb | 48 ++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4bb78b51cf..3ebd7208fb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ === HEAD +* Fix MySQL commands out of sync error when using queries with multiple result sets without retrieving all result sets (jeremyevans) + +* Allow splitting of multiple result sets into separate arrays when using multiple statements in a single query in the native MySQL adapter (jeremyevans) + * Don't include primary key indexes when parsing MSSQL indexes on JDBC (jeremyevans) * Make Dataset#insert_select return nil on PostgreSQL if disable_insert_returning is used (jeremyevans) diff --git a/lib/sequel/adapters/mysql.rb b/lib/sequel/adapters/mysql.rb index 2e8378b2e6..dbe3e8e108 100644 --- a/lib/sequel/adapters/mysql.rb +++ b/lib/sequel/adapters/mysql.rb @@ -178,7 +178,16 @@ def _execute(conn, sql, opts) rescue Mysql::Error => e raise_error(e, :disconnect=>MYSQL_DATABASE_DISCONNECT_ERRORS.match(e.message)) ensure - r.free if r + if r + r.free + # Use up all results to avoid a commands out of sync message. + if conn.respond_to?(:next_result) + while conn.next_result + r = conn.use_result + r.free if r + end + end + end end end @@ -304,21 +313,31 @@ def delete execute_dui(delete_sql){|c| c.affected_rows} end - # Yield all rows matching this dataset - def fetch_rows(sql) + # Yield all rows matching this dataset. If the dataset is set to + # split multiple statements, yield arrays of hashes one per statement + # instead of yielding results for all statements as hashes. + def fetch_rows(sql, &block) execute(sql) do |r| i = -1 cols = r.fetch_fields.map{|f| [output_identifier(f.name), MYSQL_TYPES[f.type], i+=1]} @columns = cols.map{|c| c.first} - while row = r.fetch_row - h = {} - cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v} - yield h + if opts[:split_multiple_result_sets] + s = [] + yield_rows(r, cols){|h| s << h} + yield s + else + yield_rows(r, cols, &block) end end self end + # Don't allow graphing a dataset that splits multiple statements + def graph(*) + raise(Error, "Can't graph a dataset that splits multiple result sets") if opts[:split_multiple_result_sets] + super + end + # Insert a new value into this dataset def insert(*values) execute_dui(insert_sql(*values)){|c| c.insert_id} @@ -341,6 +360,22 @@ def replace(*args) execute_dui(replace_sql(*args)){|c| c.insert_id} end + # Makes each yield arrays of rows, with each array containing the rows + # for a given result set. Does not work with graphing. So you can submit + # SQL with multiple statements and easily determine which statement + # returned which results. + # + # Modifies the row_proc of the returned dataset so that it still works + # as expected (running on the hashes instead of on the arrays of hashes). + # If you modify the row_proc afterward, note that it will receive an array + # of hashes instead of a hash. + def split_multiple_result_sets + raise(Error, "Can't split multiple statements on a graphed dataset") if opts[:graph] + ds = clone(:split_multiple_result_sets=>true) + ds.row_proc = proc{|x| x.map{|h| row_proc.call(h)}} if row_proc + ds + end + # Update the matching rows. def update(values={}) execute_dui(update_sql(values)){|c| c.affected_rows} @@ -367,6 +402,16 @@ def literal_string(v) def prepare_extend_sproc(ds) ds.extend(StoredProcedureMethods) end + + # Yield each row of the given result set r with columns cols + # as a hash with symbol keys + def yield_rows(r, cols) + while row = r.fetch_row + h = {} + cols.each{|n, p, i| v = row[i]; h[n] = (v && p) ? p.call(v) : v} + yield h + end + end end end end diff --git a/spec/adapters/mysql_spec.rb b/spec/adapters/mysql_spec.rb index 6e74c8dc89..81e76fea7d 100644 --- a/spec/adapters/mysql_spec.rb +++ b/spec/adapters/mysql_spec.rb @@ -479,14 +479,6 @@ def logger.method_missing(m, msg) @db << 'DELETE FROM items' @db[:items].first.should == nil end - - cspecify "should handle multiple select statements at once", :do, :jdbc do - @db.create_table(:items){String :name; Integer :value} - @db[:items].delete - @db[:items].insert(:name => 'tutu', :value => 1234) - @db["SELECT * FROM items; SELECT * FROM items"].all.should == \ - [{:name => 'tutu', :value => 1234}, {:name => 'tutu', :value => 1234}] - end end # Socket tests should only be run if the MySQL server is on localhost @@ -906,4 +898,44 @@ def logger.method_missing(m, msg) MYSQL_DB["SELECT CAST('25:00:00' AS time)"].single_value.should == '25:00:00' end end + + context "MySQL multiple result sets" do + before do + MYSQL_DB.create_table!(:a){Integer :a} + MYSQL_DB.create_table!(:b){Integer :b} + @ds = MYSQL_DB['SELECT * FROM a; SELECT * FROM b'] + MYSQL_DB[:a].insert(10) + MYSQL_DB[:a].insert(15) + MYSQL_DB[:b].insert(20) + MYSQL_DB[:b].insert(25) + end + after do + MYSQL_DB.drop_table(:a, :b) + end + + specify "should combine all results by default" do + @ds.all.should == [{:a=>10}, {:a=>15}, {:b=>20}, {:b=>25}] + end + + specify "should split results returned into arrays if split_multiple_result_sets is used" do + @ds.split_multiple_result_sets.all.should == [[{:a=>10}, {:a=>15}], [{:b=>20}, {:b=>25}]] + end + + specify "should have regular row_procs work when splitting multiple result sets" do + @ds.row_proc = proc{|x| x[x.keys.first] *= 2; x} + @ds.split_multiple_result_sets.all.should == [[{:a=>20}, {:a=>30}], [{:b=>40}, {:b=>50}]] + end + + specify "should use the columns from the first result set when splitting result sets" do + @ds.split_multiple_result_sets.columns.should == [:a] + end + + specify "should not allow graphing a dataset that splits multiple statements" do + proc{@ds.split_multiple_result_sets.graph(:b, :b=>:a)}.should raise_error(Sequel::Error) + end + + specify "should not allow splitting a graphed dataset" do + proc{MYSQL_DB[:a].graph(:b, :b=>:a).split_multiple_result_sets}.should raise_error(Sequel::Error) + end + end end