Skip to content

Commit

Permalink
Allow splitting of multiple result sets into separate arrays when usi…
Browse files Browse the repository at this point in the history
…ng 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.
  • Loading branch information
jeremyevans committed Sep 9, 2009
1 parent d04684c commit f15c1f9
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 15 deletions.
4 changes: 4 additions & 0 deletions 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)
Expand Down
59 changes: 52 additions & 7 deletions lib/sequel/adapters/mysql.rb
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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
48 changes: 40 additions & 8 deletions spec/adapters/mysql_spec.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit f15c1f9

Please sign in to comment.