module Clear::SQL::Query::Fetch
  # :no_doc:
  protected def fetch_result_set(h : Hash(String, ::Clear::SQL::Any), rs, &block) : Bool
    return false unless rs.move_next

    loop do
      rs.each_column do |col|
        h[col] = rs.read
      end

      yield(h)

      break unless rs.move_next
    end

    true
  ensure
    rs.close
  end

  # Fetch the data using CURSOR.
  # This will prevent Clear to load all the data from the database into memory.
  # This is useful if you need to retrieve and update a large dataset.
  def fetch_with_cursor(count = 1_000, &block : Hash(String, ::Clear::SQL::Any) -> Nil)
    trigger_before_query

    Clear::SQL.transaction do |cnx|
      cursor_name = "__cursor_#{Time.local.to_unix ^ (rand * 0xfffffff).to_i}__"

      cursor_declaration = "DECLARE #{cursor_name} CURSOR FOR #{to_sql}"

      Clear::SQL.log_query(cursor_declaration) { cnx.exec(cursor_declaration) }

      h = {} of String => ::Clear::SQL::Any

      we_loop = true

      while we_loop
        fetch_query = "FETCH #{count} FROM #{cursor_name}"

        rs = Clear::SQL.log_query(fetch_query) { cnx.query(fetch_query) }

        o = Array(Hash(String, ::Clear::SQL::Any)).new(initial_capacity: count)

        we_loop = fetch_result_set(h, rs) { |x| o << x.dup }

        o.each { |hash| yield(hash) }
      end
    end
  end

  # Helpers to fetch a SELECT with only one row and one column return.
  def scalar(type : T.class) forall T
    trigger_before_query

    sql = to_sql

    Clear::SQL.log_query sql do
      Clear::SQL::ConnectionPool.with_connection(connection_name, &.scalar(sql)).as(T)
    end
  end

  # Return the first line of the query as Hash(String, ::Clear::SQL::Any), or nil
  # if not found
  def first
    fetch_first
  end

  def first!
    fetch_first!
  end

  # Alias for `first` because first is redefined in Collection::Base
  # object to return a model instead.
  def fetch_first
    limit(1).fetch(fetch_all: true) { |x| return x }

    nil
  end

  def fetch_first!
    if o = fetch_first
      o
    else
      raise Clear::SQL::RecordNotFoundError.new
    end
  end

  # Return an array with all the rows fetched.
  def to_a : Array(Hash(String, ::Clear::SQL::Any))
    trigger_before_query

    h = {} of String => ::Clear::SQL::Any

    sql = self.to_sql

    Clear::SQL::ConnectionPool.with_connection(connection_name) do |cnx|
      rs = Clear::SQL.log_query(sql) { cnx.query(sql) }

      o = [] of Hash(String, ::Clear::SQL::Any)
      fetch_result_set(h, rs) { |x| o << x.dup }

      o
    ensure
      rs.try &.close
    end
  end

  # Fetch the result set row per row
  # `fetch_all` optional parameter is helpful in transactional environment, so it stores
  # the result and close the resultset before starting to call yield over the data
  # preventing creation of a new connection if you need to call SQL into the
  # yielded block.
  #
  # ```crystal
  # # This is wrong: The connection is still busy retrieving the users:
  # Clear::SQL.select.from("users").fetch do |u|
  #   Clear::SQL.select.from("posts").where { u["id"] == posts.id }
  # end
  #
  # # Instead, use `fetch_all`
  # # Clear will store the value of the result set in memory
  # # before calling the block, and the connection is now ready to handle
  # # another query.
  # Clear::SQL.select.from("users").fetch(fetch_all:true) do |u|
  #   Clear::SQL.select.from("posts").where { u["id"] == posts.id }
  # end
  # ```
  def fetch(fetch_all = false, &block : Hash(String, ::Clear::SQL::Any) -> Nil)
    trigger_before_query

    h = {} of String => ::Clear::SQL::Any

    sql = self.to_sql

    Clear::SQL::ConnectionPool.with_connection(connection_name) do |cnx|
      rs = Clear::SQL.log_query(sql) { cnx.query(sql) }

      if fetch_all
        o = [] of Hash(String, ::Clear::SQL::Any)
        fetch_result_set(h, rs) { |x| o << x.dup }
        o.each { |x| yield(x) }
      else
        fetch_result_set(h, rs) { |x| yield(x) }
      end
    end
  end
end
