# postgres.result

> A module of postgres's API.

`postgres.result` — the rows a query returned, and the typed accessors.

`Connection.query` hands back a `Result`: an owned `PGresult` plus the column
metadata read out of it once.  Cells are read on demand -- libpq has already
materialised the whole result set in its own memory, so there is nothing to be
gained by copying it eagerly, and a `Result` costs one `PQclear` no matter how
many rows it holds.

`Row` is the other half, and it is a **snapshot**: `Result.row` copies one
row's cells into owned `String`s, so the `Row` stays valid after the `Result`
that produced it is destroyed.  That is what makes

```mojo
def newest(mut conn: Connection) raises -> Row:
    var res = conn.query("SELECT id, name FROM t ORDER BY id DESC LIMIT 1")
    return res.row(0)          # `res` is cleared here; the Row survives
```

work, and it is why `for row in res:` yields values rather than references.
Column *names* would be expensive to copy per row, so both types share one
`ArcPointer[_Columns]` -- the names and OIDs, read once from the `PGresult`.

**Typed accessors do not check the column's OID.**  `row.int64("n")` parses
the cell's text as an `int8` whatever the server called it, because the
contract is "you asked for int64, we parse the text as int64"; a mismatch
surfaces as the codec's own error, which names both the type and the offending
text (``postgres.text: invalid int8 text: 'abc'``).  Call `column_oid` first if
you would rather dispatch on what the server actually sent.

**NULL is not the empty string.**  libpq renders both as ``""``; `is_null` is
the only thing that separates them.  Every typed accessor raises on NULL, so a
nullable column is read with `Row.is_null` first, or with `Row.opt_text`.

## Structs

### `Row`

```mojo
struct Row
```

One result row, copied out of the `PGresult` and independent of it.

Obtained from `Result.row` or by iterating a `Result`.  Every cell is an
owned `String` plus a NULL flag, so a `Row` may outlive the `Result` --
and may be stored, returned, or copied freely.

Each accessor comes in two overloads: one taking a 0-based column index,
one taking the column name.  The name form resolves through the shared
column metadata, so it works on a `Row` whose `Result` is long gone.

Example:

```mojo
var res = conn.query("SELECT id, name, note FROM t")
for row in res:
    print(row.int64("id"), row.text("name"), row.opt_text("note"))
```

Implements: `Copyable`, `Movable`

#### `__init__`

```mojo
def __init__(out self, var values: List[String], var nulls: List[Bool], cols: ArcPointer[_Columns])
```

Build a snapshot from already-copied cells.

Args:

- `values`: One text-format value per column.
- `nulls`: True where the cell is SQL NULL, parallel to `values`.
- `cols`: The result's shared column metadata.

#### `num_cols`

```mojo
def num_cols(self) -> Int
```

The number of columns in this row.

#### `column_name`

```mojo
def column_name(self, col: Int) -> String
```

The name of column `col`.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

#### `column_index`

```mojo
def column_index(self, name: String) -> Int
```

The index of the column called `name`.

Args:

- `name`: The column name, matched exactly (see `_Columns.index_of`).

Raises: Error: If there is no such column; the message lists the ones
there are.

#### `column_oid`

```mojo
def column_oid(self, col: Int) -> UInt32
```

The ``pg_type.oid`` of column `col`.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

#### `is_null`

```mojo
def is_null(self, col: Int) -> Bool
```

Whether column `col` is SQL NULL.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

```mojo
def is_null(self, col: String) -> Bool
```

Whether the column named `col` is SQL NULL.

Args:

- `col`: The column name.

Raises: Error: If there is no such column.

#### `opt_text`

```mojo
def opt_text(self, col: Int) -> Optional[String]
```

Column `col` as text, or `None` when it is SQL NULL.

The NULL-tolerant counterpart to `Row.text`.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

```mojo
def opt_text(self, col: String) -> Optional[String]
```

The column named `col` as text, or `None` when it is SQL NULL.

Args:

- `col`: The column name.

Raises: Error: If there is no such column.

#### `text`

```mojo
def text(self, col: Int) -> String
```

Column `col` exactly as the server rendered it.

The escape hatch for every type this module has no accessor for, and
the only way to read the values the typed decoders refuse -- an
``infinity`` date, a ``BC`` timestamp, an array, a composite.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range, or the cell is SQL NULL.

```mojo
def text(self, col: String) -> String
```

The column named `col`, exactly as the server rendered it.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, or the cell is SQL NULL.

#### `bool`

```mojo
def bool(self, col: Int) -> Bool
```

Column `col` decoded as `bool` (`t`/`f`).

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL or is not valid `bool` text.

```mojo
def bool(self, col: String) -> Bool
```

The column named `col`, decoded as `bool`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not valid `bool`.

#### `int16`

```mojo
def int16(self, col: Int) -> Int16
```

Column `col` decoded as `int2`.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not an in-range `int2`.

```mojo
def int16(self, col: String) -> Int16
```

The column named `col`, decoded as `int2`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not an in-range `int2`.

#### `int32`

```mojo
def int32(self, col: Int) -> Int32
```

Column `col` decoded as `int4`.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not an in-range `int4`.

```mojo
def int32(self, col: String) -> Int32
```

The column named `col`, decoded as `int4`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not an in-range `int4`.

#### `int64`

```mojo
def int64(self, col: Int) -> Int64
```

Column `col` decoded as `int8`.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not an in-range `int8`.

```mojo
def int64(self, col: String) -> Int64
```

The column named `col`, decoded as `int8`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not an in-range `int8`.

#### `float32`

```mojo
def float32(self, col: Int) -> Float32
```

Column `col` decoded as `float4`.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not a valid float.

```mojo
def float32(self, col: String) -> Float32
```

The column named `col`, decoded as `float4`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not a valid float.

#### `float64`

```mojo
def float64(self, col: Int) -> Float64
```

Column `col` decoded as `float8`.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not a valid float.

```mojo
def float64(self, col: String) -> Float64
```

The column named `col`, decoded as `float8`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not a valid float.

#### `numeric`

```mojo
def numeric(self, col: Int) -> String
```

Column `col` as a `numeric` literal, kept as text.

`numeric` is arbitrary precision; turning it into a `Float64` would
silently lose digits, so this hands back exactly the digits the server
sent -- trailing zeros of the column's scale included.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range, or the cell is SQL NULL.

```mojo
def numeric(self, col: String) -> String
```

The column named `col` as a `numeric` literal, kept as text.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, or the cell is SQL NULL.

#### `bytea`

```mojo
def bytea(self, col: Int) -> List[UInt8]
```

Column `col` decoded as `bytea`.

Both text sub-formats are accepted: the ``\x``-prefixed hex the
server emits by default, and the legacy octal-escape format.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not valid `bytea`.

```mojo
def bytea(self, col: String) -> List[UInt8]
```

The column named `col`, decoded as `bytea`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not valid `bytea`.

#### `date_days`

```mojo
def date_days(self, col: Int) -> Int32
```

Column `col` decoded as `date`, in days since 1970-01-01.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, the text is not a valid date, or the
date is ``infinity``/``-infinity`` or a ``BC`` year -- neither
of which the epoch encoding can carry.  Read `Row.text` for
those.

```mojo
def date_days(self, col: String) -> Int32
```

The column named `col`, decoded as `date` in days since the epoch.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the date
cannot be expressed as an epoch day count.

#### `time_micros`

```mojo
def time_micros(self, col: Int) -> Int64
```

Column `col` decoded as `time`, in microseconds since midnight.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, or the text is not a valid time.

```mojo
def time_micros(self, col: String) -> Int64
```

The column named `col`, decoded as `time` in microseconds.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the text
is not a valid time.

#### `timestamp_micros`

```mojo
def timestamp_micros(self, col: Int) -> Int64
```

Column `col` decoded as naive `timestamp`, in epoch microseconds.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, the text is not a valid timestamp, or
it is infinite or ``BC``.

```mojo
def timestamp_micros(self, col: String) -> Int64
```

The column named `col`, decoded as naive `timestamp`.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the
timestamp cannot be expressed in epoch microseconds.

#### `timestamptz_micros`

```mojo
def timestamptz_micros(self, col: Int) -> Int64
```

Column `col` decoded as `timestamptz`, in **UTC** epoch microseconds.

The server renders `timestamptz` in the session's ``TimeZone``, with
the offset appended; the offset is applied here, so the result is UTC
regardless of what the session is set to.

Args:

- `col`: The 0-based column index.

Raises: Error: If the cell is NULL, the text is not a valid zoned
timestamp, or it is infinite or ``BC``.

```mojo
def timestamptz_micros(self, col: String) -> Int64
```

The column named `col`, decoded as `timestamptz` in UTC micros.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, the cell is NULL, or the
timestamp cannot be expressed in epoch microseconds.

#### `uuid`

```mojo
def uuid(self, col: Int) -> String
```

Column `col` as `uuid` text, e.g. ``"a0ee-...-4f1b"``.

The server always renders a `uuid` in the canonical lower-case
8-4-4-4-12 form, so this is the raw text.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range, or the cell is SQL NULL.

```mojo
def uuid(self, col: String) -> String
```

The column named `col` as `uuid` text.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, or the cell is SQL NULL.

#### `json`

```mojo
def json(self, col: Int) -> String
```

Column `col` as `json`/`jsonb` text.

Not parsed -- this tin has no JSON model.  `json` comes back exactly
as it was stored; `jsonb` comes back in the server's normalised form
(keys reordered, whitespace canonical).

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range, or the cell is SQL NULL.

```mojo
def json(self, col: String) -> String
```

The column named `col` as `json`/`jsonb` text.

Args:

- `col`: The column name.

Raises: Error: If there is no such column, or the cell is SQL NULL.

### `Result`

```mojo
struct Result
```

The rows one command returned, owning the underlying `PGresult`.

Built by `Connection.query` from an already status-checked result handle,
and cleared exactly once when it is destroyed.  Not copyable: two owners
would mean two `PQclear`s.

A command that returns no rows -- an ``INSERT``, a ``CREATE TABLE`` --
still yields a `Result`, with `num_rows` and `num_cols` both ``0`` and
`affected_rows` carrying the count.

Example:

```mojo
var res = conn.query("SELECT id, name FROM t WHERE id > $1",
                     Params().int64(10))
print(res.num_rows(), "rows")
for row in res:
    print(row.int64("id"), row.text("name"))
```

Implements: `Iterable`, `Movable`, `Sized`

#### `IteratorType`

```mojo
comptime IteratorType: Iterator = _RowIter[iterable_origin]
```

The iterator `for row in res:` uses; it yields `Row` snapshots.

#### `__init__`

```mojo
def __init__(out self, res: Int)
```

Take ownership of an already status-checked result handle.

The status check belongs to the caller: by the time a `Result` exists
the command has succeeded, so none of its accessors have to answer for
`PGRES_FATAL_ERROR`.  `Connection.query` is the only intended caller.

Args:

- `res`: A `PGresultPtr` whose `PQresultStatus` is `PGRES_TUPLES_OK`,
    `PGRES_COMMAND_OK` or `PGRES_EMPTY_QUERY`.  This `Result` now
    owns it and will `PQclear` it exactly once.

Raises: Error: Only if libpq itself could not be reached, which cannot
happen once a `Connection` exists.

#### `__deinit__`

```mojo
def __deinit__(deinit self)
```

Clear the `PGresult`, releasing libpq's copy of every row.

#### `num_rows`

```mojo
def num_rows(self) -> Int
```

The number of rows.

#### `num_cols`

```mojo
def num_cols(self) -> Int
```

The number of columns.

#### `__len__`

```mojo
def __len__(self) -> Int
```

The number of rows, so `len(res)` reads naturally.

#### `column_name`

```mojo
def column_name(self, col: Int) -> String
```

The name of column `col`.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

#### `column_index`

```mojo
def column_index(self, name: String) -> Int
```

The index of the column called `name`.

Matching is exact, as in `PQfnumber`: an unquoted SQL identifier
arrives folded to lower case, so ask for ``"userid"``, not
``"userId"``.

Args:

- `name`: The column name.

Raises: Error: If there is no such column; the message lists the ones
there are.

#### `column_oid`

```mojo
def column_oid(self, col: Int) -> UInt32
```

The ``pg_type.oid`` of column `col`.

The hook for callers who want to dispatch on the server's actual type
rather than assert one -- `Row`'s typed accessors deliberately do not
consult it.

Args:

- `col`: The 0-based column index.

Raises: Error: If `col` is out of range.

#### `affected_rows`

```mojo
def affected_rows(self) -> Int
```

Rows inserted, updated, deleted or retrieved by the command.

Raises: Error: If libpq reported a count that is not an integer, which
would mean a libpq bug rather than a user error.

#### `is_null`

```mojo
def is_null(self, row: Int, col: Int) -> Bool
```

Whether the cell at (`row`, `col`) is SQL NULL.

Args:

- `row`: The 0-based row index.
- `col`: The 0-based column index.

Raises: Error: If either index is out of range.

#### `text`

```mojo
def text(self, row: Int, col: Int) -> String
```

The cell at (`row`, `col`) as the server rendered it.

Args:

- `row`: The 0-based row index.
- `col`: The 0-based column index.

Raises: Error: If either index is out of range, or the cell is SQL NULL --
check `Result.is_null` first, or read the whole row with
`Result.row` and use `Row.opt_text`.

#### `row`

```mojo
def row(self, row: Int) -> Row
```

Copy row `row` into a standalone `Row` snapshot.

The snapshot owns its cells and shares only the column metadata, so it
stays usable after this `Result` is destroyed.

Args:

- `row`: The 0-based row index.

Raises: Error: If `row` is out of range.

#### `__iter__`

```mojo
def __iter__(ref self) -> _RowIter[origin_of(self)]
```

Iterate the rows, yielding one `Row` snapshot at a time.

Rows are copied lazily -- only the row the loop is on exists as a
`Row` -- so a large result costs one row's worth of `String`s at a
time on top of what libpq already holds.
