lib/sqlite3/statement.rb



module SQLite3

  # A statement represents a prepared-but-unexecuted SQL query. It will rarely
  # (if ever) be instantiated directly by a client, and is most often obtained
  # via the Database#prepare method.
  class Statement

    # This is any text that followed the first valid SQL statement in the text
    # with which the statement was initialized. If there was no trailing text,
    # this will be the empty string.
    attr_reader :remainder

    # The underlying opaque handle used to access the SQLite @driver.
    attr_reader :handle

    # Create a new statement attached to the given Database instance, and which
    # encapsulates the given SQL text. If the text contains more than one
    # statement (i.e., separated by semicolons), then the #remainder property
    # will be set to the trailing text.
    def initialize(db, sql, utf_16 = false)
      raise ArgumentError, "nil argument passed as sql text" unless sql
      @db = db
      @driver = @db.driver
      @closed = false
      @results = @columns = nil
      @utf_16 = utf_16
      result, @handle, @remainder = @driver.prepare(@db.handle, sql)
      Error.check(result, @db)
    end

    # Closes the statement by finalizing the underlying statement
    # handle. The statement must not be used after being closed.
    def close
      must_be_open!
      @closed = true
      @driver.finalize(@handle)
    end

    # Returns true if the underlying statement has been closed.
    def closed?
      @closed
    end

    # Binds the given variables to the corresponding placeholders in the SQL
    # text.
    #
    # See Database#execute for a description of the valid placeholder
    # syntaxes.
    #
    # Example:
    #
    #   stmt = db.prepare("select * from table where a=? and b=?")
    #   stmt.bind_params(15, "hello")
    #
    # See also #execute, #bind_param, Statement#bind_param, and
    # Statement#bind_params.
    def bind_params(*bind_vars)
      index = 1
      bind_vars.flatten.each do |var|
        if Hash === var
          var.each { |key, val| bind_param key, val }
        else
          bind_param index, var
          index += 1
        end
      end
    end

    # Binds value to the named (or positional) placeholder. If +param+ is a
    # Fixnum, it is treated as an index for a positional placeholder.
    # Otherwise it is used as the name of the placeholder to bind to.
    #
    # See also #bind_params.
    def bind_param(param, value)
      must_be_open!
      reset! if active?
      if Fixnum === param
        case value
        when Bignum then
          @driver.bind_int64(@handle, param, value)
        when Integer then
          if value >= (2 ** 31)
            @driver.bind_int64(@handle, param, value)
          else
            @driver.bind_int(@handle, param, value)
          end
        when Numeric then
          @driver.bind_double(@handle, param, value.to_f)
        when nil then
          @driver.bind_null(@handle, param)
        else
          @driver.bind_string(@handle, param, value.to_s)
        end
      else
        param = param.to_s
        param = ":#{param}" unless param[0] == ?:
        index = @driver.bind_parameter_index(@handle, param)
        raise Exception, "no such bind parameter '#{param}'" if index == 0
        bind_param index, value
      end
    end

    # Execute the statement. This creates a new ResultSet object for the
    # statement's virtual machine. If a block was given, the new ResultSet will
    # be yielded to it; otherwise, the ResultSet will be returned.
    #
    # Any parameters will be bound to the statement using #bind_params.
    #
    # Example:
    #
    #   stmt = db.prepare("select * from table")
    #   stmt.execute do |result|
    #     ...
    #   end
    #
    # See also #bind_params, #execute!.
    def execute(*bind_vars)
      must_be_open!
      reset! if active?

      bind_params(*bind_vars) unless bind_vars.empty?
      @results = ResultSet.new(@db, self, @utf_16)

      if block_given?
        yield @results
      else
        return @results
      end
    end

    # Execute the statement. If no block was given, this returns an array of
    # rows returned by executing the statement. Otherwise, each row will be
    # yielded to the block.
    #
    # Any parameters will be bound to the statement using #bind_params.
    #
    # Example:
    #
    #   stmt = db.prepare("select * from table")
    #   stmt.execute! do |row|
    #     ...
    #   end
    #
    # See also #bind_params, #execute.
    def execute!(*bind_vars)
      result = execute(*bind_vars)
      rows = [] unless block_given?
      while row = result.next
        if block_given?
          yield row
        else
          rows << row
        end
      end
      rows
    end

    # Resets the statement. This is typically done internally, though it might
    # occassionally be necessary to manually reset the statement.
    def reset!(clear_result=true)
      @driver.reset(@handle)
      @results = nil if clear_result
    end

    # Returns true if the statement is currently active, meaning it has an
    # open result set.
    def active?
      not @results.nil?
    end

    # Return an array of the column names for this statement. Note that this
    # may execute the statement in order to obtain the metadata; this makes it
    # a (potentially) expensive operation.
    def columns
      get_metadata unless @columns
      return @columns
    end

    # Return an array of the data types for each column in this statement. Note
    # that this may execute the statement in order to obtain the metadata; this
    # makes it a (potentially) expensive operation.
    def types
      get_metadata unless @types
      return @types
    end

    # A convenience method for obtaining the metadata about the query. Note
    # that this will actually execute the SQL, which means it can be a
    # (potentially) expensive operation.
    def get_metadata
      must_be_open!

      @columns = []
      @types = []

      column_count = @driver.column_count(@handle)
      column_count.times do |column|
        @columns << @driver.column_name(@handle, column)
        @types << @driver.column_decltype(@handle, column)
      end
    end
    private :get_metadata

    # Performs a sanity check to ensure that the statement is not
    # closed. If it is, an exception is raised.
    def must_be_open! # :nodoc:
      if @closed
        raise SQLite3::Exception, "cannot use a closed statement"
      end
    end

  end

end