diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b82f46..4cc5751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ ### 0.8.0 -* Refined TableWrapper interface + +0.8.0 introduces a few backward-incompatible changes. + +* `Connection#ResultSetEnumerator` is renamed to `Connection::ResultSet` +* `Connection#query` method will return a ResultSet object instead of an Array + * `Connection#enumerate` method is now retired, and just a synonym for query method + * Partially consumed ResultSet must be closed explicitly +* `ResultSet#each` method will return an enumerator when block is not given +* Refined TableWrapper interface with external [sql_helper](https://github.com/junegunn/sql_helper) gem + * The use of `JDBCHelper::SQL` is deprecated * Added MariaDB connector * Added SQLite connector -* Deprecated the use of `JDBCHelper::SQL` -* `ResultSetEnumerator#each` returns an enumerator when block is not given ### 0.7.7 / 2013/01/0? * `PreparedStatment`s and `TableWrapper`s now inherit the fetch size of the connection diff --git a/README.md b/README.md index 677bf56..a30e283 100644 --- a/README.md +++ b/README.md @@ -128,10 +128,6 @@ conn.query('SELECT a, b, c FROM T') do |row| row.to_h # Row as a Hash end -# Returns an array of rows when block is not given -rows = conn.query('SELECT b FROM T') -uniq_rows = rows.uniq - # You can even nest queries conn.query('SELECT a FROM T') do |row1| conn.query("SELECT * FROM T_#{row1.a}") do |row2| @@ -139,12 +135,19 @@ conn.query('SELECT a FROM T') do |row1| end end -# `enumerate' method returns an Enumerable object if block is not given. -# When the result set of the query is expected to be large and you wish to -# chain enumerators, `enumerate' is much preferred over `query'. (which returns the -# array of the entire rows) -conn.enumerate('SELECT * FROM LARGE_T').each_slice(1000) do |slice| - slice.each do | row | +# Connection::ResultSet object is returned when block is not given +# - ResultSet is automatically closed when entirely iterated +rows = conn.query('SELECT * FROM T') +uniq_rows = rows.to_a.uniq + +# However, partially consumed ResultSet objects *must be closed* manually +rset = conn.query('SELECT * FROM T') +rows = rset.take(2) +rset.close + +# Enumerator chain +conn.query('SELECT * FROM LARGE_T').each_slice(1000).with_index do |slice, idx| + slice.each do |row| # ... end end diff --git a/lib/jdbc-helper/connection.rb b/lib/jdbc-helper/connection.rb index 58e5271..c9e1af0 100644 --- a/lib/jdbc-helper/connection.rb +++ b/lib/jdbc-helper/connection.rb @@ -8,7 +8,7 @@ require 'jdbc-helper/connection/prepared_statement' require 'jdbc-helper/connection/callable_statement' require 'jdbc-helper/connection/statement_pool' -require 'jdbc-helper/connection/result_set_enumerator' +require 'jdbc-helper/connection/result_set' require 'jdbc-helper/connection/row' require 'jdbc-helper/wrapper/object_wrapper' @@ -241,18 +241,18 @@ def transaction status == :committed end - # Executes an SQL and returns the count of the update rows or a ResultSetEnumerator object + # Executes an SQL and returns the count of the update rows or a ResultSet object # depending on the type of the given statement. - # If a ResultSetEnumerator is returned, it must be enumerated or closed. + # If a ResultSet is returned, it must be enumerated or closed. # @param [String] qstr SQL string - # @return [Fixnum|ResultSetEnumerator] + # @return [Fixnum|ResultSet] def execute(qstr) check_closed stmt = @spool.take begin if stmt.execute(qstr) - ResultSetEnumerator.send(:new, stmt.getResultSet) { @spool.give stmt } + ResultSet.send(:new, stmt.getResultSet) { @spool.give stmt } else rset = stmt.getUpdateCount @spool.give stmt @@ -277,16 +277,14 @@ def update(qstr) # Executes a select query. # When a code block is given, each row of the result is passed to the block one by one. - # If a code block not given, this method will return the array of the entire result rows. - # (which can be pretty inefficient when the result set is large. In such cases, use enumerate instead.) - # - # The concept of statement object of JDBC is encapsulated, so there's no need to do additional task, - # when you nest select queries, for example. + # If not given, ResultSet is returned, which can be used to enumerate through the result set. + # ResultSet is closed automatically when all the rows in the result set is consumed. # + # @example Nested querying # conn.query("SELECT a FROM T") do | trow | - # conn.query("SELECT * FROM U_#{trow.a}") do | urow | - # # ... and so on ... - # end + # conn.query("SELECT * FROM U_#{trow.a}").each_slice(10) do | urows | + # # ... + # end # end # @param [String] qstr SQL string # @yield [JDBCHelper::Connection::Row] @@ -294,40 +292,24 @@ def update(qstr) def query(qstr, &blk) check_closed - @spool.with do | stmt | - rset = stmt.execute_query(qstr) - process_and_close_rset(rset, &blk) - end - end - - # Returns an enumerable object of the query result. - # "enumerate" method is preferable when dealing with a large result set, - # since it doesn't have to build a large array. - # - # The returned enumerator is automatically closed after enumeration. - # - # conn.enumerate('SELECT * FROM T').each_slice(10) do | slice | - # slice.each { | row | print row } - # puts - # end - # - # @param [String] qstr SQL string - # @yield [JDBCHelper::Connection::Row] Yields each record if block is given - # @return [JDBCHelper::Connection::ResultSetEnumerator] Returns an enumerator if block is not given - def enumerate(qstr, &blk) - return query(qstr, &blk) if block_given? - - check_closed - stmt = @spool.take begin rset = stmt.execute_query(qstr) - return ResultSetEnumerator.send(:new, rset) { @spool.give stmt } - rescue Exception + rescue Exception => e @spool.give stmt raise end + + enum = ResultSet.send(:new, rset) { @spool.give stmt } + if block_given? + enum.each do |row| + yield row + end + else + enum + end end + alias enumerate query # Adds a statement to be executed in batch # Adds to the batch @@ -480,24 +462,6 @@ def create_statement # :nodoc: stmt end - def process_and_close_rset(rset) # :nodoc: - enum = ResultSetEnumerator.send :new, rset - rows = [] - - begin - enum.each do | row | - if block_given? - yield row - else - rows << row - end - end - block_given? ? nil : rows - ensure - enum.close - end - end - def close_pstmt pstmt @pstmts.delete pstmt end diff --git a/lib/jdbc-helper/connection/prepared_statement.rb b/lib/jdbc-helper/connection/prepared_statement.rb index 6824423..980bf58 100644 --- a/lib/jdbc-helper/connection/prepared_statement.rb +++ b/lib/jdbc-helper/connection/prepared_statement.rb @@ -27,13 +27,13 @@ def close @java_obj = nil end - # @return [Fixnum|ResultSetEnumerator] + # @return [Fixnum|ResultSet] def execute(*params) check_closed set_params(params) if @java_obj.execute - ResultSetEnumerator.new(@java_obj.getResultSet) + ResultSet.new(@java_obj.getResultSet) else @java_obj.getUpdateCount end @@ -52,19 +52,16 @@ def query(*params, &blk) check_closed set_params(params) - # sorry, ignoring privacy - @conn.send(:process_and_close_rset, @java_obj.execute_query, &blk) - end - - # @return [JDBCHelper::Connection::ResultSetEnumerator] - def enumerate(*params, &blk) - check_closed - - return query(*params, &blk) if block_given? - - set_params(params) - ResultSetEnumerator.new(@java_obj.execute_query) + enum = ResultSet.new(@java_obj.execute_query) + if block_given? + enum.each do |row| + yield row + end + else + enum + end end + alias enumerate query # Adds to the batch # @return [NilClass] diff --git a/lib/jdbc-helper/connection/result_set_enumerator.rb b/lib/jdbc-helper/connection/result_set.rb similarity index 59% rename from lib/jdbc-helper/connection/result_set_enumerator.rb rename to lib/jdbc-helper/connection/result_set.rb index 5acf941..68817c1 100644 --- a/lib/jdbc-helper/connection/result_set_enumerator.rb +++ b/lib/jdbc-helper/connection/result_set.rb @@ -7,49 +7,47 @@ module JDBCHelper class Connection # Class for enumerating query results. # Automatically closed after used. When not used, you must close it explicitly by calling "close". -class ResultSetEnumerator +class ResultSet include Enumerable def each return enum_for(:each) unless block_given? return if closed? - count = -1 - begin - while @rset.next - idx = 0 - # Oracle returns numbers in NUMERIC type, which can be of any precision. - # So, we retrieve the numbers in String type not to lose their precision. - # This can be quite annoying when you're just working with integers, - # so I tried the following code to automatically convert integer string into integer - # when it's obvious. However, the performance drop is untolerable. - # Thus, commented out. - # - # if v && @cols_meta[i-1] == java.sql.Types::NUMERIC && v !~ /[\.e]/i - # v.to_i - # else - # v - # end - yield Connection::Row.new( - @col_labels, - @col_labels_d, - @getters.map { |gt| - case gt - when :getBigNum - v = @rset.getBigDecimal idx+=1 - @rset.was_null ? nil : v.toPlainString.to_i - when :getBigDecimal - v = @rset.getBigDecimal idx+=1 - @rset.was_null ? nil : BigDecimal.new(v.toPlainString) - else - v = @rset.send gt, idx+=1 - @rset.was_null ? nil : v - end - }, count += 1) - end - ensure - close + while @nrow + idx = 0 + # Oracle returns numbers in NUMERIC type, which can be of any precision. + # So, we retrieve the numbers in String type not to lose their precision. + # This can be quite annoying when you're just working with integers, + # so I tried the following code to automatically convert integer string into integer + # when it's obvious. However, the performance drop is untolerable. + # Thus, commented out. + # + # if v && @cols_meta[i-1] == java.sql.Types::NUMERIC && v !~ /[\.e]/i + # v.to_i + # else + # v + # end + row = Connection::Row.new( + @col_labels, + @col_labels_d, + @getters.map { |gt| + case gt + when :getBigNum + v = @rset.getBigDecimal idx+=1 + @rset.was_null ? nil : v.toPlainString.to_i + when :getBigDecimal + v = @rset.getBigDecimal idx+=1 + @rset.was_null ? nil : BigDecimal.new(v.toPlainString) + else + v = @rset.send gt, idx+=1 + @rset.was_null ? nil : v + end + }, @rownum += 1) + close unless @nrow = @rset.next + yield row end + close end def close @@ -116,9 +114,11 @@ def initialize(rset, &close_callback) # :nodoc: end + @rownum = -1 + @nrow = @rset.next @closed = false end -end#ResultSetEnumerator +end#ResultSet end#Connection end#JDBCHelper diff --git a/lib/jdbc-helper/wrapper/function_wrapper.rb b/lib/jdbc-helper/wrapper/function_wrapper.rb index 32bcc64..80b1644 100644 --- a/lib/jdbc-helper/wrapper/function_wrapper.rb +++ b/lib/jdbc-helper/wrapper/function_wrapper.rb @@ -27,7 +27,7 @@ def initialize conn, name def call(*args) pstmt = @connection.prepare("select #{name}(#{args.map{'?'}.join ','})#{@suffix}") begin - pstmt.query(*args)[0][0] + pstmt.query(*args).to_a[0][0] ensure pstmt.close end diff --git a/lib/jdbc-helper/wrapper/sequence_wrapper.rb b/lib/jdbc-helper/wrapper/sequence_wrapper.rb index 46af228..9b20ed8 100644 --- a/lib/jdbc-helper/wrapper/sequence_wrapper.rb +++ b/lib/jdbc-helper/wrapper/sequence_wrapper.rb @@ -31,13 +31,13 @@ def initialize(conn, name) # Increments the sequence and returns the value # @return [Fixnum] def nextval - @connection.query(@nextval_sql)[0][0].to_i + @connection.query(@nextval_sql).to_a[0][0].to_i end # Returns the incremented value of the sequence # @return [Fixnum] def currval - @connection.query(@currval_sql)[0][0].to_i + @connection.query(@currval_sql).to_a[0][0].to_i end # Recreates the sequence. Cannot be undone. diff --git a/lib/jdbc-helper/wrapper/table_wrapper.rb b/lib/jdbc-helper/wrapper/table_wrapper.rb index 2ec1069..ce99950 100644 --- a/lib/jdbc-helper/wrapper/table_wrapper.rb +++ b/lib/jdbc-helper/wrapper/table_wrapper.rb @@ -52,7 +52,7 @@ class TableWrapper < ObjectWrapper def count *where sql, *binds = SQLHelper.count :table => name, :where => @query_where + where, :prepared => true pstmt = prepare :count, sql - pstmt.query(*binds)[0][0].to_i + pstmt.query(*binds).to_a[0][0].to_i end # Sees if the table is empty @@ -222,7 +222,7 @@ def fetch_size fsz, &block # Executes a select SQL for the table and returns an Enumerable object, # or yields each row if block is given. - # @return [JDBCHelper::Connection::ResultSetEnumerator] + # @return [JDBCHelper::Connection::ResultSet] # @since 0.4.0 def each &block sql, *binds = SQLHelper.select( diff --git a/test/test_connection.rb b/test/test_connection.rb index a4954f4..10751e0 100644 --- a/test/test_connection.rb +++ b/test/test_connection.rb @@ -187,9 +187,10 @@ def test_fetch_size def test_query_enumerate each_connection do | conn | - # Query without a block => Array + # Query without a block => ResultSet query_result = conn.query get_one_two - assert query_result.is_a? Array + assert query_result.is_a? JDBCHelper::Connection::ResultSet + query_result = query_result.to_a assert_equal 2, query_result.length check_one_two(query_result.first) assert_equal query_result.first, query_result.last @@ -203,7 +204,7 @@ def test_query_enumerate assert_equal 2, count # Enumerate - enum = conn.enumerate(get_one_two) + enum = conn.query(get_one_two) assert enum.is_a? Enumerable assert enum.closed? == false a = enum.to_a @@ -213,19 +214,20 @@ def test_query_enumerate # Enumerator chain cnt = 0 - enum = conn.enumerate(get_one_two) + enum = conn.query(get_one_two) assert_equal false, enum.closed? enum.each.each.each.each_slice(1) do |slice| assert_equal Array, slice.class assert_equal 1, slice.length cnt += 1 - assert_equal false, enum.closed? + assert_equal cnt == 2, enum.closed? end assert_equal true, enum.closed? assert_equal 2, cnt + # Enumerator chain 2 cnt = 0 - enum = conn.enumerate(get_one_two) + enum = conn.query(get_one_two) assert_equal false, enum.closed? enum.each.each.each.with_index.each_slice(2).each do |slice| assert_equal Array, slice.class @@ -235,10 +237,31 @@ def test_query_enumerate assert_equal cnt, slice[0][1] assert_equal cnt + 1, slice[1][1] cnt += 2 - assert_equal false, enum.closed? + assert_equal cnt == 2, enum.closed? end assert_equal true, enum.closed? assert_equal 2, cnt + assert_equal 0, enum.to_a.length + + enum = conn.query(' + select 1 a from dual union all + select 2 a from dual union all + select 3 a from dual') + + row = enum.take(1).first + assert_equal 0, row.rownum + assert_equal false, enum.closed? + + row = enum.take(1).first + assert_equal 1, row.rownum + assert_equal false, enum.closed? + + row = enum.take(1).first + assert_equal 2, row.rownum + assert_equal true, enum.closed? + + assert_equal [], enum.take(1) + assert_equal true, enum.closed? end end @@ -246,17 +269,18 @@ def test_enumerate_errors each_connection do |conn| # On error, Statement object must be returned to the StatementPool (JDBCHelper::Constants::MAX_STATEMENT_NESTING_LEVEL * 2).times do |i| - conn.enumerate('xxx') rescue nil + conn.query('xxx') rescue nil end - assert_equal 'OK', conn.query("select 'OK' from dual")[0][0] + assert_equal 'OK', conn.query("select 'OK' from dual").to_a[0][0] end end def test_deep_nesting nest = lambda { |str, lev| if lev > 0 - "conn.query('select 1 from dual') do |r#{lev}| + "conn.query('select 1 a from dual union all select 2 a from dual') do |r#{lev}| #{nest.call str, lev - 1} + break end" else str @@ -352,7 +376,8 @@ def test_prepared_query_enumerate # Query without a block => Array query_result = sel.query - assert query_result.is_a? Array + assert query_result.is_a? JDBCHelper::Connection::ResultSet + query_result = query_result.to_a assert_equal 2, query_result.length check_one_two(query_result.first) @@ -365,7 +390,7 @@ def test_prepared_query_enumerate assert_equal 2, count # Enumerate - enum = sel.enumerate + enum = sel.query assert enum.is_a? Enumerable assert enum.closed? == false a = enum.to_a @@ -504,13 +529,13 @@ def test_setter_timestamp if @type == :mysql conn.prepare("insert into #{TEST_TABLE} (a, b, c) values (?, ?, ?)").update(ts, d, t) # MySQL doesn't have subsecond precision - assert [lt, (lt * 0.001).round * 1000].include?(conn.query("select a from #{TEST_TABLE}")[0][0].getTime) + assert [lt, (lt * 0.001).round * 1000].include?(conn.query("select a from #{TEST_TABLE}").to_a[0][0].getTime) # The JDBC spec states that java.sql.Dates have _no_ time component # http://bugs.mysql.com/bug.php?id=2876 - assert_equal d.getTime, conn.query("select b from #{TEST_TABLE}")[0][0].getTime + assert_equal d.getTime, conn.query("select b from #{TEST_TABLE}").to_a[0][0].getTime # http://stackoverflow.com/questions/907170/java-getminutes-and-gethours - t2 = conn.query("select c from #{TEST_TABLE}")[0][0] + t2 = conn.query("select c from #{TEST_TABLE}").to_a[0][0] cal = java.util.Calendar.getInstance cal.setTime(t) cal2 = java.util.Calendar.getInstance @@ -527,7 +552,7 @@ def test_setter_timestamp reset_test_table_ts conn ts = Time.now conn.prepare("insert into #{TEST_TABLE} (a) values (?)").update(ts) - got = conn.query("select a from #{TEST_TABLE}")[0][0] + got = conn.query("select a from #{TEST_TABLE}").to_a[0][0] arr = [ ts.to_i * 1000, (ts.to_f.round) * 1000, # MySQL @@ -641,7 +666,7 @@ def test_execute each_connection do | conn | reset_test_table conn - rse_class = JDBCHelper::Connection::ResultSetEnumerator + rse_class = JDBCHelper::Connection::ResultSet # Connection#execute assert_equal 1, conn.execute("insert into #{TEST_TABLE} values (0, 'A')") @@ -689,8 +714,8 @@ def test_statement_pool_leakage assert_equal 20, 20.times.select { conn.execute(q).close - conn.enumerate(q).close - conn.query q + conn.query(q).close + conn.query(q).to_a conn.update u conn.execute(q).count == 1