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