Cursor

class ref Cursor
  """
  Non-sendable result set from an ad-hoc query (Connection.query()).
  Supports fetch and close only — no binding, no re-execution.
  close() frees the underlying SQLHSTMT entirely.
  """

  var _hstmt: Pointer[None] tag
  let _conn_alive: _AliveFlag ref
  var _closed: Bool
  var _last_warnings: (Warnings | None)
  var _col_bindings: (_ColumnBindings | None)

  new ref _create(
    hstmt: Pointer[None] tag,
    conn_alive: _AliveFlag ref,
    opts: OdbcOptions = OdbcOptions) ?
  =>
    _hstmt = hstmt
    _conn_alive = conn_alive
    _closed = false
    _last_warnings = None
    // Set up column bindings immediately — cursor is already open.
    // Raises error on failure so Connection.query() can report it.
    _col_bindings = _ColumnBindings(hstmt, opts)?

  fun ref fetch(): (Row | EndOfRows | FetchError) =>
    """
    Fetch the next row.
    """
    if _closed then return FetchError(CursorClosed) end
    if not _conn_alive.is_alive() then
      return FetchError(FetchConnectionClosed)
    end

    let rc = @SQLFetch(_hstmt)

    if rc == ODBCConstants.sql_no_data() then
      return EndOfRows
    end

    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return FetchError(DriverFetchError, diag)
    end

    match _col_bindings
    | let cb: _ColumnBindings => cb.build_row()
    else FetchError(DriverFetchError)
    end

  fun ref fetch_into(row: MutableRow): (MutableRow | EndOfRows | FetchError) =>
    """
    Fetch the next row into a reusable MutableRow.
    """
    if _closed then return FetchError(CursorClosed) end
    if not _conn_alive.is_alive() then
      return FetchError(FetchConnectionClosed)
    end

    let rc = @SQLFetch(_hstmt)

    if rc == ODBCConstants.sql_no_data() then
      return EndOfRows
    end

    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return FetchError(DriverFetchError, diag)
    end

    match _col_bindings
    | let cb: _ColumnBindings => cb.build_row_into(row)
    else FetchError(DriverFetchError)
    end

  fun cancel_token(): CancelToken =>
    """
    Return a sendable token that can cancel this cursor's
    in-progress operation from another actor.

    The token captures a raw copy of the SQLHSTMT pointer. It does not
    track whether the cursor has been closed. Calling cancel() on a
    token after close() invokes SQLCancel on a freed handle — undefined
    behavior. The caller must ensure all outstanding tokens are discarded
    before calling close().
    """
    CancelToken(_hstmt)

  fun ref values(): CursorIterator =>
    """
    Return an iterator for use with Pony's `for` loop.
    Yields (Row val | FetchError) — match on each result.
    """
    CursorIterator(this)

  fun ref last_warnings(): (Warnings | None) =>
    _last_warnings

  fun ref close() =>
    """
    Free the SQLHSTMT. Idempotent.

    Any CancelTokens obtained from cancel_token() become invalid after
    this call. Using a token after close() is undefined behavior — see
    cancel_token() for the lifetime contract.

    If the connection has already been closed, the driver freed this
    handle transitively via SQLFreeHandle(SQL_HANDLE_DBC); in that case
    we only mark ourselves closed without a second SQLFreeHandle call
    (which would be UB on a dangling handle).
    """
    if _closed then return end
    if _conn_alive.is_alive() then
      @SQLFreeHandle(ODBCConstants.handle_stmt(), _hstmt)
    end
    _hstmt = Pointer[None]
    _closed = true
    _col_bindings = None

  fun _final() =>
    if (not _closed) and _conn_alive.is_alive() then
      @SQLFreeHandle(ODBCConstants.handle_stmt(), _hstmt)
    end