# frozen-string-literal: true

require 'java'
require_relative 'utils/stored_procedures'

module Sequel
  module JDBC
    # Make it accesing the java.sql hierarchy more ruby friendly.
    module JavaSQL
      include_package 'java.sql'
    end

    # Used to identify a jndi connection and to extract the jndi
    # resource name.
    JNDI_URI_REGEXP = /\Ajdbc:jndi:(.+)/
    
    # Contains procs keyed on subadapter type that extend the
    # given database object so it supports the correct database type.
    DATABASE_SETUP = {}

    # Create custom NativeException alias for nicer access, and also so that
    # JRuby 9.2+ so it doesn't use the deprecated ::NativeException
    NativeException = Java::JavaLang::Exception
    
    # Default database error classes
    DATABASE_ERROR_CLASSES = [NativeException]
    if JRUBY_VERSION < '9.2'
      # On JRuby <9.2, still include ::NativeException, as it is still needed in some cases
      DATABASE_ERROR_CLASSES << ::NativeException
    end
    DATABASE_ERROR_CLASSES.freeze

    # Allow loading the necessary JDBC support via a gem.
    def self.load_gem(name)
      require "jdbc/#{name.to_s.downcase}"
    rescue LoadError
      # jdbc gem not used, hopefully the user has the .jar in their CLASSPATH
    else
      if defined?(::Jdbc) && ( ::Jdbc.const_defined?(name) rescue nil )
        jdbc_module = ::Jdbc.const_get(name) # e.g. Jdbc::SQLite3
        jdbc_module.load_driver if jdbc_module.respond_to?(:load_driver)
      end
    end

    # Attempt to load the JDBC driver class, which should be specified as a string
    # containing the driver class name (which JRuby should autoload).
    # Note that the string is evaled, so this method is not safe to call with
    # untrusted input.
    # Raise a Sequel::AdapterNotFound if evaluating the class name raises a NameError.
    def self.load_driver(drv, gem=nil)
      load_gem(gem) if gem
      if drv.is_a?(String)
        eval drv
      else
        *try, last = drv
        try.each do |try_drv|
          begin
            return eval(try_drv)
          rescue NameError
          end
        end

        eval last
      end
    rescue NameError
      raise Sequel::AdapterNotFound, "#{drv} not loaded#{", try installing jdbc-#{gem.to_s.downcase} gem" if gem}"
    end

    class TypeConvertor
      CONVERTORS = convertors = {}
      %w'Boolean Float Double Int Long Short'.each do |meth|
        x = x = convertors[meth.to_sym] = Object.new
        class_eval("def x.call(r, i) v = r.get#{meth}(i); v unless r.wasNull end", __FILE__, __LINE__)
      end
      %w'Object Array String Time Date Timestamp BigDecimal Blob Bytes Clob'.each do |meth|
        x = x = convertors[meth.to_sym] = Object.new
        class_eval("def x.call(r, i) r.get#{meth}(i) end", __FILE__, __LINE__)
      end
      x = convertors[:RubyTime] = Object.new
      def x.call(r, i)
        if v = r.getTime(i)
          Sequel.string_to_time("#{v.to_string}.#{sprintf('%03i', v.getTime.divmod(1000).last)}")
        end
      end
      x = convertors[:RubyDate] = Object.new
      def x.call(r, i)
        if v = r.getDate(i)
          Date.civil(v.getYear + 1900, v.getMonth + 1, v.getDate)
        end
      end
      x = convertors[:RubyTimestamp] = Object.new
      def x.call(r, i)
        if v = r.getTimestamp(i)
          Sequel.database_to_application_timestamp([v.getYear + 1900, v.getMonth + 1, v.getDate, v.getHours, v.getMinutes, v.getSeconds, v.getNanos])
        end
      end
      x = convertors[:RubyBigDecimal] = Object.new
      def x.call(r, i)
        if v = r.getBigDecimal(i)
          ::Kernel::BigDecimal(v.to_string)
        end
      end
      x = convertors[:RubyBlob] = Object.new
      def x.call(r, i)
        if v = r.getBytes(i)
          Sequel::SQL::Blob.new(String.from_java_bytes(v))
        end
      end
      x = convertors[:RubyClob] = Object.new
      def x.call(r, i)
        if v = r.getClob(i)
          v.getSubString(1, v.length)
        end
      end
      x = convertors[:RubyArray] = Object.new
      def x.call(r, i)
        if v = r.getArray(i)
          v.array.to_ary
        end
      end 

      MAP = Hash.new(convertors[:Object])
      types = Java::JavaSQL::Types

      {
        :BOOLEAN => :Boolean,
        :CHAR => :String,
        :DOUBLE => :Double,
        :FLOAT => :Double,
        :INTEGER => :Int,
        :LONGNVARCHAR => :String,
        :LONGVARCHAR => :String,
        :NCHAR => :String,
        :REAL => :Float,
        :SMALLINT => :Short,
        :TINYINT => :Short,
        :VARCHAR => :String,
      }.each do |type, meth|
        MAP[types.const_get(type)] = convertors[meth]
      end
      BASIC_MAP = MAP.dup

      {
        :ARRAY => :Array,
        :BINARY => :Blob,
        :BLOB => :Blob,
        :CLOB => :Clob,
        :DATE => :Date,
        :DECIMAL => :BigDecimal,
        :LONGVARBINARY => :Blob,
        :NCLOB => :Clob,
        :NUMERIC => :BigDecimal,
        :TIME => :Time,
        :TIMESTAMP => :Timestamp,
        :VARBINARY => :Blob,
      }.each do |type, meth|
        BASIC_MAP[types.const_get(type)] = convertors[meth]
        MAP[types.const_get(type)] = convertors[:"Ruby#{meth}"]
      end
      MAP.freeze
      BASIC_MAP.freeze
    end

    class Database < Sequel::Database
      set_adapter_scheme :jdbc
      
      # The Java database driver we are using (should be a Java class)
      attr_reader :driver
      
      # Whether to convert some Java types to ruby types when retrieving rows.
      # True by default, can be set to false to roughly double performance when
      # fetching rows.
      attr_accessor :convert_types

      # The fetch size to use for JDBC Statement objects created by this database.
      # By default, this is nil so a fetch size is not set explicitly.
      attr_accessor :fetch_size

      # Map of JDBC type ids to callable objects that return appropriate ruby values.
      attr_reader :type_convertor_map

      # Map of JDBC type ids to callable objects that return appropriate ruby or java values.
      attr_reader :basic_type_convertor_map

      # Execute the given stored procedure with the give name. If a block is
      # given, the stored procedure should return rows.
      def call_sproc(name, opts = OPTS)
        args = opts[:args] || []
        sql = "{call #{name}(#{args.map{'?'}.join(',')})}"
        synchronize(opts[:server]) do |conn|
          begin
            cps = conn.prepareCall(sql)

            i = 0
            args.each{|arg| set_ps_arg(cps, arg, i+=1)}

            if defined?(yield)
              yield log_connection_yield(sql, conn){cps.executeQuery}
            else
              log_connection_yield(sql, conn){cps.executeUpdate}
              if opts[:type] == :insert
                last_insert_id(conn, opts)
              end
            end
          rescue *DATABASE_ERROR_CLASSES => e
            raise_error(e)
          ensure
            cps.close if cps
          end
        end
      end
         
      # Connect to the database using JavaSQL::DriverManager.getConnection, and falling back
      # to driver.new.connect if the driver is known.
      def connect(server)
        opts = server_opts(server)
        conn = if jndi?
          get_connection_from_jndi
        else
          args = [uri(opts)]
          args.concat([opts[:user], opts[:password]]) if opts[:user] && opts[:password]
          begin
            JavaSQL::DriverManager.setLoginTimeout(opts[:login_timeout]) if opts[:login_timeout]
            raise StandardError, "skipping regular connection" if opts[:jdbc_properties]
            JavaSQL::DriverManager.getConnection(*args)
          rescue StandardError, *DATABASE_ERROR_CLASSES => e
            raise e unless driver
            # If the DriverManager can't get the connection - use the connect
            # method of the driver. (This happens under Tomcat for instance)
            props = Java::JavaUtil::Properties.new
            if opts && opts[:user] && opts[:password]
              props.setProperty("user", opts[:user])
              props.setProperty("password", opts[:password])
            end
            opts[:jdbc_properties].each{|k,v| props.setProperty(k.to_s, v)} if opts[:jdbc_properties]
            begin
              c = driver.new.connect(args[0], props)
              raise(Sequel::DatabaseError, 'driver.new.connect returned nil: probably bad JDBC connection string') unless c
              c
            rescue StandardError, *DATABASE_ERROR_CLASSES => e2
              if e2.respond_to?(:message=) && e2.message != e.message
                e2.message = "#{e2.message}\n#{e.class.name}: #{e.message}"
              end
              raise e2
            end
          end
        end
        setup_connection_with_opts(conn, opts)
      end

      # Close given adapter connections, and delete any related prepared statements.
      def disconnect_connection(c)
        @connection_prepared_statements_mutex.synchronize{@connection_prepared_statements.delete(c)}
        c.close
      end
      
      def execute(sql, opts=OPTS, &block)
        return call_sproc(sql, opts, &block) if opts[:sproc]
        return execute_prepared_statement(sql, opts, &block) if [Symbol, Dataset].any?{|c| sql.is_a?(c)}
        synchronize(opts[:server]) do |conn|
          statement(conn) do |stmt|
            if block
              if size = fetch_size
                stmt.setFetchSize(size)
              end
              yield log_connection_yield(sql, conn){stmt.executeQuery(sql)}
            else
              case opts[:type]
              when :ddl
                log_connection_yield(sql, conn){stmt.execute(sql)}
              when :insert
                log_connection_yield(sql, conn){execute_statement_insert(stmt, sql)}
                opts = Hash[opts]
                opts[:stmt] = stmt
                last_insert_id(conn, opts)
              else
                log_connection_yield(sql, conn){stmt.executeUpdate(sql)}
              end
            end
          end
        end
      end
      alias execute_dui execute

      def execute_ddl(sql, opts=OPTS)
        opts = Hash[opts]
        opts[:type] = :ddl
        execute(sql, opts)
      end
      
      def execute_insert(sql, opts=OPTS)
        opts = Hash[opts]
        opts[:type] = :insert
        execute(sql, opts)
      end

      def freeze
        @type_convertor_map.freeze
        @basic_type_convertor_map.freeze
        super
      end

      # Use the JDBC metadata to get a list of foreign keys for the table.
      def foreign_key_list(table, opts=OPTS)
        m = output_identifier_meth
        schema, table = metadata_schema_and_table(table, opts)
        foreign_keys = {}

        metadata(:getImportedKeys, nil, schema, table) do |r|
          next unless fk_name = r[:fk_name]

          key_seq = r[:key_seq]
          columns = [key_seq, m.call(r[:fkcolumn_name])]
          key = [key_seq, m.call(r[:pkcolumn_name])]

          if fk = foreign_keys[fk_name]
            fk[:columns] << columns
            fk[:key] << key
          else
            foreign_keys[fk_name] = {
              :name=>m.call(fk_name),
              :columns=>[columns],
              :table=>m.call(r[:pktable_name]),
              :key=>[key]
            }
          end
        end

        fk_keys = [:columns, :key]
        foreign_keys.values.each do |fk|
          fk_keys.each do |k|
            fk[k].sort!.map!{|_, v| v}
          end
        end
      end

      # Use the JDBC metadata to get the index information for the table.
      def indexes(table, opts=OPTS)
        m = output_identifier_meth
        schema, table = metadata_schema_and_table(table, opts)
        indexes = {}
        metadata(:getIndexInfo, nil, schema, table, false, true) do |r|
          next unless name = r[:column_name]
          next if respond_to?(:primary_key_index_re, true) and r[:index_name] =~ primary_key_index_re 
          i = indexes[m.call(r[:index_name])] ||= {:columns=>[], :unique=>[false, 0].include?(r[:non_unique])}
          i[:columns] << m.call(name)
        end
        indexes
      end 

      # Whether or not JNDI is being used for this connection.
      def jndi?
        !!(uri =~ JNDI_URI_REGEXP)
      end
      
      # All tables in this database
      def tables(opts=OPTS)
        get_tables('TABLE', opts)
      end
      
      # The uri for this connection.  You can specify the uri
      # using the :uri, :url, or :database options.  You don't
      # need to worry about this if you use Sequel.connect
      # with the JDBC connectrion strings.
      def uri(opts=OPTS)
        opts = @opts.merge(opts)
        ur = opts[:uri] || opts[:url] || opts[:database]
        ur =~ /^\Ajdbc:/ ? ur : "jdbc:#{ur}"
      end

      # All views in this database
      def views(opts=OPTS)
        get_tables('VIEW', opts)
      end

      private
         
      # Call the DATABASE_SETUP proc directly after initialization,
      # so the object always uses sub adapter specific code.  Also,
      # raise an error immediately if the connection doesn't have a
      # uri, since JDBC requires one.
      def adapter_initialize
        @connection_prepared_statements = {}
        @connection_prepared_statements_mutex = Mutex.new
        @fetch_size = @opts[:fetch_size] ? typecast_value_integer(@opts[:fetch_size]) : default_fetch_size
        @convert_types = typecast_value_boolean(@opts.fetch(:convert_types, true))
        raise(Error, "No connection string specified") unless uri
        
        resolved_uri = jndi? ? get_uri_from_jndi : uri
        setup_type_convertor_map_early

        @driver = if (match = /\Ajdbc:([^:]+)/.match(resolved_uri)) && (prok = Sequel::Database.load_adapter(match[1].to_sym, :map=>DATABASE_SETUP, :subdir=>'jdbc'))
          prok.call(self)
        else
          @opts[:driver]
        end        

        setup_type_convertor_map
      end
      
      # Yield the native prepared statements hash for the given connection
      # to the block in a thread-safe manner.
      def cps_sync(conn, &block)
        @connection_prepared_statements_mutex.synchronize{yield(@connection_prepared_statements[conn] ||= {})}
      end

      def database_error_classes
        DATABASE_ERROR_CLASSES
      end

      def database_exception_sqlstate(exception, opts)
        if database_exception_use_sqlstates?
          _database_exception_sqlstate(exception, opts)
        end
      end

      def _database_exception_sqlstate(exception, opts)
        16.times do
          return exception.getSQLState if exception.respond_to?(:getSQLState)
          break unless exception.respond_to?(:cause) && (exception = exception.cause)
        end

        nil
      end

      # Whether the JDBC subadapter should use SQL states for exception handling, true by default.
      def database_exception_use_sqlstates?
        true
      end

      def dataset_class_default
        Dataset
      end

      # Raise a disconnect error if the SQL state of the cause of the exception indicates so.
      def disconnect_error?(exception, opts)
        super || (_database_exception_sqlstate(exception, opts) =~ /^08/)
      end

      # Execute the prepared statement.  If the provided name is a
      # dataset, use that as the prepared statement, otherwise use
      # it as a key to look it up in the prepared_statements hash.
      # If the connection we are using has already prepared an identical
      # statement, use that statement instead of creating another.
      # Otherwise, prepare a new statement for the connection, bind the
      # variables, and execute it.
      def execute_prepared_statement(name, opts=OPTS)
        args = opts[:arguments]
        if name.is_a?(Dataset)
          ps = name
          name = ps.prepared_statement_name
        else
          ps = prepared_statement(name)
        end
        sql = ps.prepared_sql
        synchronize(opts[:server]) do |conn|
          if name and cps = cps_sync(conn){|cpsh| cpsh[name]} and cps[0] == sql
            cps = cps[1]
          else
            log_connection_yield("CLOSE #{name}", conn){cps[1].close} if cps
            if name
              opts = Hash[opts]
              opts[:name] = name
            end
            cps = log_connection_yield("PREPARE#{" #{name}:" if name} #{sql}", conn){prepare_jdbc_statement(conn, sql, opts)}
            if size = fetch_size
              cps.setFetchSize(size)
            end
            cps_sync(conn){|cpsh| cpsh[name] = [sql, cps]} if name
          end
          i = 0
          args.each{|arg| set_ps_arg(cps, arg, i+=1)}
          msg = "EXECUTE#{" #{name}" if name}"
          if ps.log_sql
            msg += " ("
            msg << sql
            msg << ")"
          end
          begin
            if defined?(yield)
              yield log_connection_yield(msg, conn, args){cps.executeQuery}
            else
              case opts[:type]
              when :ddl
                log_connection_yield(msg, conn, args){cps.execute}
              when :insert
                log_connection_yield(msg, conn, args){execute_prepared_statement_insert(cps)}
                opts = Hash[opts]
                opts[:prepared] = true
                opts[:stmt] = cps
                last_insert_id(conn, opts)
              else
                log_connection_yield(msg, conn, args){cps.executeUpdate}
              end
            end
          rescue *DATABASE_ERROR_CLASSES => e
            raise_error(e)
          ensure
            cps.close unless name
          end
        end
      end

      # Execute the prepared insert statement
      def execute_prepared_statement_insert(stmt)
        stmt.executeUpdate
      end
      
      # Execute the insert SQL using the statement
      def execute_statement_insert(stmt, sql)
        stmt.executeUpdate(sql)
      end

      # The default fetch size to use for statements.  Nil by default, so that the
      # default for the JDBC driver is used.
      def default_fetch_size
        nil
      end
      
      # Gets the connection from JNDI.
      def get_connection_from_jndi
        jndi_name = JNDI_URI_REGEXP.match(uri)[1]
        Java::JavaxNaming::InitialContext.new.lookup(jndi_name).connection
      end
            
      # Gets the JDBC connection uri from the JNDI resource.
      def get_uri_from_jndi
        conn = get_connection_from_jndi
        conn.meta_data.url
      ensure
        conn.close if conn
      end
      
      # Backbone of the tables and views support.
      def get_tables(type, opts)
        ts = []
        m = output_identifier_meth
        if schema = opts[:schema]
          schema = schema.to_s
        end
        metadata(:getTables, nil, schema, nil, [type].to_java(:string)){|h| ts << m.call(h[:table_name])}
        ts
      end

      # Support Date objects used in bound variables
      def java_sql_date(date)
        Java::JavaSql::Date.new(Time.local(date.year, date.month, date.day).to_i * 1000)
      end

      # Support DateTime objects used in bound variables
      def java_sql_datetime(datetime)
        ts = Java::JavaSql::Timestamp.new(Time.local(datetime.year, datetime.month, datetime.day, datetime.hour, datetime.min, datetime.sec).to_i * 1000)
        ts.setNanos((datetime.sec_fraction * 1000000000).to_i)
        ts
      end

      # Support fractional seconds for Time objects used in bound variables
      def java_sql_timestamp(time)
        ts = Java::JavaSql::Timestamp.new(time.to_i * 1000)
        ts.setNanos(time.nsec)
        ts
      end 
      
      def log_connection_execute(conn, sql)
        statement(conn){|s| log_connection_yield(sql, conn){s.execute(sql)}}
      end

      # By default, there is no support for determining the last inserted
      # id, so return nil.  This method should be overridden in
      # subadapters.
      def last_insert_id(conn, opts)
        nil
      end
      
      # Yield the metadata for this database
      def metadata(*args, &block)
        synchronize do |c|
          result = c.getMetaData.public_send(*args)
          begin
            metadata_dataset.send(:process_result_set, result, &block)
          ensure
            result.close
          end
        end
      end

      # Return the schema and table suitable for use with metadata queries.
      def metadata_schema_and_table(table, opts)
        im = input_identifier_meth(opts[:dataset])
        schema, table = schema_and_table(table)
        schema ||= opts[:schema]
        schema = im.call(schema) if schema
        table = im.call(table)
        [schema, table]
      end
      
      # Created a JDBC prepared statement on the connection with the given SQL.
      def prepare_jdbc_statement(conn, sql, opts)
        conn.prepareStatement(sql)
      end

      # Java being java, you need to specify the type of each argument
      # for the prepared statement, and bind it individually.  This
      # guesses which JDBC method to use, and hopefully JRuby will convert
      # things properly for us.
      def set_ps_arg(cps, arg, i)
        case arg
        when Integer
          cps.setLong(i, arg)
        when Sequel::SQL::Blob
          cps.setBytes(i, arg.to_java_bytes)
        when String
          cps.setString(i, arg)
        when Float
          cps.setDouble(i, arg)
        when TrueClass, FalseClass
          cps.setBoolean(i, arg)
        when NilClass
          set_ps_arg_nil(cps, i)
        when DateTime
          cps.setTimestamp(i, java_sql_datetime(arg))
        when Date
          cps.setDate(i, java_sql_date(arg))
        when Time
          cps.setTimestamp(i, java_sql_timestamp(arg))
        when Java::JavaSql::Timestamp
          cps.setTimestamp(i, arg)
        when Java::JavaSql::Date
          cps.setDate(i, arg)
        else
          cps.setObject(i, arg)
        end
      end

      # Use setString with a nil value by default, but this doesn't work on all subadapters.
      def set_ps_arg_nil(cps, i)
        cps.setString(i, nil)
      end
      
      # Return the connection.  Can be overridden in subadapters for database specific setup.
      def setup_connection(conn)
        conn
      end

      # Setup the connection using the given connection options. Return the connection.  Can be overridden in subadapters for database specific setup.
      def setup_connection_with_opts(conn, opts)
        setup_connection(conn)
      end

      def schema_column_set_db_type(schema)
        case schema[:type]
        when :string
          if schema[:db_type] =~ /\A(character( varying)?|n?(var)?char2?)\z/i && schema[:column_size] > 0
            schema[:db_type] += "(#{schema[:column_size]})"
          end
        when :decimal
          if schema[:db_type] =~ /\A(decimal|numeric)\z/i && schema[:column_size] > 0 && schema[:scale] >= 0
            schema[:db_type] += "(#{schema[:column_size]}, #{schema[:scale]})"
          end
        end
      end
      
      def schema_parse_table(table, opts=OPTS)
        m = output_identifier_meth(opts[:dataset])
        schema, table = metadata_schema_and_table(table, opts)
        pks, ts = [], []
        metadata(:getPrimaryKeys, nil, schema, table) do |h|
          next if schema_parse_table_skip?(h, schema)
          pks << h[:column_name]
        end
        schemas = []
        metadata(:getColumns, nil, schema, table, nil) do |h|
          next if schema_parse_table_skip?(h, schema)
          s = {
            :type=>schema_column_type(h[:type_name]),
            :db_type=>h[:type_name],
            :default=>(h[:column_def] == '' ? nil : h[:column_def]),
            :allow_null=>(h[:nullable] != 0),
            :primary_key=>pks.include?(h[:column_name]),
            :column_size=>h[:column_size],
            :scale=>h[:decimal_digits],
            :remarks=>h[:remarks]
          }
          if s[:primary_key]
            s[:auto_increment] = h[:is_autoincrement] == "YES"
          end
          s[:max_length] = s[:column_size] if s[:type] == :string
          if s[:db_type] =~ /number|numeric|decimal/i && s[:scale] == 0
            s[:type] = :integer
          end
          schema_column_set_db_type(s)
          schemas << h[:table_schem] unless schemas.include?(h[:table_schem])
          ts << [m.call(h[:column_name]), s]
        end
        if schemas.length > 1
          raise Error, 'Schema parsing in the jdbc adapter resulted in columns being returned for a table with the same name in multiple schemas.  Please explicitly qualify your table with a schema.'
        end
        ts
      end
      
      # Skip tables in the INFORMATION_SCHEMA when parsing columns.
      def schema_parse_table_skip?(h, schema)
        h[:table_schem] == 'INFORMATION_SCHEMA'
      end

      # Called after loading subadapter-specific code, overridable by subadapters.
      def setup_type_convertor_map
      end

      # Called before loading subadapter-specific code, necessary so that subadapter initialization code
      # that runs queries works correctly.  This cannot be overridden in subadapters.
      def setup_type_convertor_map_early
        @type_convertor_map = TypeConvertor::MAP.merge(Java::JavaSQL::Types::TIMESTAMP=>method(:timestamp_convert))
        @basic_type_convertor_map = TypeConvertor::BASIC_MAP.dup
      end

      # Yield a new statement object, and ensure that it is closed before returning.
      def statement(conn)
        stmt = conn.createStatement
        yield stmt
      rescue *DATABASE_ERROR_CLASSES => e
        raise_error(e)
      ensure
        stmt.close if stmt
      end

      # A conversion method for timestamp columns.  This is used to make sure timestamps are converted using the
      # correct timezone.
      def timestamp_convert(r, i)
        if v = r.getTimestamp(i)
          to_application_timestamp([v.getYear + 1900, v.getMonth + 1, v.getDate, v.getHours, v.getMinutes, v.getSeconds, v.getNanos])
        end
      end
    end
    
    class Dataset < Sequel::Dataset
      include StoredProcedures

      PreparedStatementMethods = prepared_statements_module(
        "sql = self; opts = Hash[opts]; opts[:arguments] = bind_arguments",
        Sequel::Dataset::UnnumberedArgumentMapper,
        %w"execute execute_dui") do
          private

          def execute_insert(sql, opts=OPTS)
            sql = self
            opts = Hash[opts]
            opts[:arguments] = bind_arguments
            opts[:type] = :insert
            super
          end
      end
      
      StoredProcedureMethods = prepared_statements_module(
        "sql = @opts[:sproc_name]; opts = Hash[opts]; opts[:args] = @opts[:sproc_args]; opts[:sproc] = true",
        Sequel::Dataset::StoredProcedureMethods,
        %w"execute execute_dui") do
          private

          def execute_insert(sql, opts=OPTS)
            sql = @opts[:sproc_name]
            opts = Hash[opts]
            opts[:args] = @opts[:sproc_args]
            opts[:sproc] = true
            opts[:type] = :insert
            super
          end
      end
      
      def fetch_rows(sql, &block)
        execute(sql){|result| process_result_set(result, &block)}
        self
      end
      
      # Set the fetch size on JDBC ResultSets created from the returned dataset.
      def with_fetch_size(size)
        clone(:fetch_size=>size)
      end

      # Set whether to convert Java types to ruby types in the returned dataset.
      def with_convert_types(v)
        clone(:convert_types=>v)
      end
      
      private

      # Whether we should convert Java types to ruby types for this dataset.
      def convert_types?
        ct = @opts[:convert_types]
        ct.nil? ? db.convert_types : ct
      end

      # Extend the dataset with the JDBC stored procedure methods.
      def prepare_extend_sproc(ds)
        ds.with_extend(StoredProcedureMethods)
      end

      # The type conversion proc to use for the given column number i,
      # given the type conversion map and the ResultSetMetaData.
      def type_convertor(map, meta, type, i)
        map[type]
      end

      # The basic type conversion proc to use for the given column number i,
      # given the type conversion map and the ResultSetMetaData.
      #
      # This is implemented as a separate method so that subclasses can
      # override the methods separately.
      def basic_type_convertor(map, meta, type, i)
        map[type]
      end

      def prepared_statement_modules
        [PreparedStatementMethods]
      end

      # Split out from fetch rows to allow processing of JDBC result sets
      # that don't come from issuing an SQL string.
      def process_result_set(result)
        meta = result.getMetaData
        if fetch_size = opts[:fetch_size]
          result.setFetchSize(fetch_size)
        end
        cols = []
        i = 0
        convert = convert_types?
        map = convert ? db.type_convertor_map : db.basic_type_convertor_map

        meta.getColumnCount.times do
          i += 1
          cols << [output_identifier(meta.getColumnLabel(i)), i, convert ? type_convertor(map, meta, meta.getColumnType(i), i) : basic_type_convertor(map, meta, meta.getColumnType(i), i)]
        end
        max = i
        self.columns = cols.map{|c| c[0]}

        while result.next
          row = {}
          i = -1
          while (i += 1) < max
            n, j, pr = cols[i]
            row[n] = pr.call(result, j)
          end
          yield row
        end
      ensure
        result.close
      end
    end
  end
end
