# `ExAthena.Sessions.SchemaStore`
[🔗](https://github.com/udin-io/ex_athena/blob/v0.7.1/lib/ex_athena/sessions/schema_store.ex#L1)

Behaviour for row-shaped session storage (sessions / messages / snapshots).

This is a companion to `ExAthena.Sessions.Store` (the append-only event-log
behaviour). While the event-log store is optimised for sequential replay, this
behaviour exposes O(1)-lookup CRUD primitives that sub-ticket 2–4 (resume,
checkpoint, fork, rewind) will call.

## Tables

  * **sessions** — one row per session; keyed by `session_id`.
  * **messages** — one row per message; keyed by `(session_id, seq,
    message_id)` so range scans over a session's history are O(log n).
  * **snapshots** — one row per snapshot; keyed by `(session_id,
    message_id, snapshot_id)` so every snapshot associated with a
    fork-point can be enumerated with a prefix scan.

`Stores.ETS` additionally maintains an internal `:ex_athena_snapshot_index`
table (`snapshot_id → {session_id, message_id}`) that makes `get_snapshot/1`
an O(1) lookup. This index is an implementation detail of `Stores.ETS` and is
not part of this behaviour contract.

## Row shapes

See the `t:session/0`, `t:message/0`, and `t:snapshot/0` typespecs below.
All timestamps are ISO 8601 strings produced by
`DateTime.utc_now() |> DateTime.to_iso8601()`.

# `message`

```elixir
@type message() :: %{
  :id =&gt; message_id(),
  :session_id =&gt; session_id(),
  :role =&gt; :system | :user | :assistant | :tool,
  :content =&gt; map(),
  :ts =&gt; String.t(),
  optional(:seq) =&gt; integer()
}
```

# `message_id`

```elixir
@type message_id() :: String.t()
```

# `session`

```elixir
@type session() :: %{
  :id =&gt; session_id(),
  optional(:parent_id) =&gt; session_id() | nil,
  optional(:title) =&gt; String.t() | nil,
  optional(:model) =&gt; String.t() | nil,
  optional(:created_at) =&gt; String.t(),
  optional(:updated_at) =&gt; String.t(),
  optional(:metadata) =&gt; map()
}
```

# `session_id`

```elixir
@type session_id() :: String.t()
```

# `snapshot`

```elixir
@type snapshot() :: %{
  id: String.t(),
  session_id: session_id(),
  message_id: message_id(),
  state: map(),
  created_at: String.t()
}
```

# `delete_messages_after`

```elixir
@callback delete_messages_after(session_id(), message_id()) :: :ok
```

# `delete_messages_for_session`
*optional* 

```elixir
@callback delete_messages_for_session(session_id()) :: :ok
```

# `delete_session`

```elixir
@callback delete_session(session_id()) :: :ok
```

# `delete_snapshots_for_session`
*optional* 

```elixir
@callback delete_snapshots_for_session(session_id()) :: :ok
```

# `get_session`

```elixir
@callback get_session(session_id()) :: {:ok, session()} | {:error, :not_found}
```

# `get_snapshot`

```elixir
@callback get_snapshot(String.t()) :: {:ok, snapshot()} | {:error, :not_found}
```

# `list_messages`

```elixir
@callback list_messages(session_id()) :: {:ok, [message()]}
```

# `list_sessions`

```elixir
@callback list_sessions() :: [session()]
```

# `list_snapshots`

```elixir
@callback list_snapshots(session_id()) :: {:ok, [snapshot()]}
```

# `put_message`

```elixir
@callback put_message(message()) :: :ok
```

# `put_session`

```elixir
@callback put_session(session()) :: :ok
```

# `put_snapshot`

```elixir
@callback put_snapshot(snapshot()) :: :ok
```

# `implements?`

```elixir
@spec implements?(module()) :: boolean()
```

Returns `true` when `mod` exports every required SchemaStore callback.

Used by `ExAthena.Session` to opportunistically dual-write to row tables
and to select the SchemaStore read path on `resume/2`. Stores that only
implement the event-log `Store` behaviour return `false`.

# `new_message_id`

```elixir
@spec new_message_id() :: message_id()
```

Generate a unique message id (16 random bytes, url-safe base64).

# `new_snapshot_id`

```elixir
@spec new_snapshot_id() :: String.t()
```

Generate a unique snapshot id (16 random bytes, url-safe base64).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
