guides/links.md

# Event Links Guide

This guide shows how to use Spector's event links feature to create many-to-many relationships between events. Links are useful for tracking ancestry, references, dependencies, and other relationships.

## Overview

Event links allow events to reference other events in the same event log. Common use cases include:

- **Ancestry tracking**: Building tree-structured data like conversation branches
- **Edit history**: Linking edited versions to their originals
- **References**: Connecting related events within the same record
- **Dependencies**: Tracking which events depend on others

## Setup

This guide demonstrates typed link tables, which add a `type` integer column to categorize different relationship types.

### Typed Link Schema

Define an Ecto schema to manage the type values:

```elixir
defmodule MyApp.TypedEventLink do
  use Ecto.Schema
  alias Ecto.Changeset

  @type link_type :: :parent | :sibling | :reference | :supersedes

  @primary_key false
  schema "typed_event_links" do
    field :event_id, :binary_id
    field :linked_id, :binary_id
    # Use explicit integer assignments for forward compatibility.
    # This ensures existing data remains valid if you rename a type.
    field :type, Ecto.Enum, values: [parent: 0, sibling: 1, reference: 2, supersedes: 3]
  end

  def changeset(link \\ %__MODULE__{}, attrs) do
    link
    |> Changeset.cast(attrs, [:event_id, :linked_id, :type])
    |> Changeset.validate_required([:event_id, :linked_id, :type])
  end

  @doc "Create a link struct for insertion"
  def new(event_id, linked_id, type) do
    %__MODULE__{event_id: event_id, linked_id: linked_id, type: type}
  end
end
```

### Document Schema with prepare_event

The `prepare_event/3` callback lets you populate link associations before an event is inserted into the event table:

```elixir
defmodule MyApp.TypedDoc do
  use Spector.Evented, events: MyApp.TypedLinkEvents, actions: [:revise]
  use Ecto.Schema
  alias Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: false}
  embedded_schema do
    field :content, :string
    field :version, :integer, default: 1
  end

  @impl true
  def prepare_event(event_changeset, previous_events, attrs) do
    case Spector.get_attr(attrs, :supersedes) do
      nil ->
        event_changeset

      superseded_id ->
        # Find the superseded event
        superseded_event = Enum.find(previous_events, &(&1.id == superseded_id))

        if superseded_event do
          # Create a typed link marking this event as superseding another
          link = MyApp.TypedEventLink.new(
            Changeset.get_field(event_changeset, :id),
            superseded_id,
            :supersedes
          )
          Changeset.put_assoc(event_changeset, :links, [link])
        else
          event_changeset
        end
    end
  end

  def changeset(changeset, attrs) when changeset.action == :revise do
    current_version = Changeset.get_field(changeset, :version, 0)

    changeset
    |> Changeset.cast(attrs, [:content])
    |> Changeset.put_change(:version, current_version + 1)
    |> Changeset.validate_required([:content])
  end

  def changeset(changeset, attrs) do
    changeset
    |> Changeset.cast(attrs, [:content])
    |> Changeset.validate_required([:content])
  end

  import Ecto.Query

  def get_superseding_events(event_id) do
    from(link in MyApp.TypedEventLink,
      where: link.linked_id == ^event_id and link.type == :supersedes,
      join: e in MyApp.TypedLinkEvents,
      on: e.id == link.event_id,
      select: e
    )
    |> MyApp.Repo.all()
  end

  def get_superseded_by(event_id) do
    from(link in MyApp.TypedEventLink,
      where: link.event_id == ^event_id and link.type == :supersedes,
      join: e in MyApp.TypedLinkEvents,
      on: e.id == link.linked_id,
      select: e
    )
    |> MyApp.Repo.all()
  end
end
```

### Events Module

```elixir
defmodule MyApp.TypedLinkEvents do
  use Spector.Events,
    table: "typed_link_events",
    links: [links: {MyApp.TypedEventLink, :linked_id}],
    schemas: [MyApp.TypedDoc],
    repo: MyApp.Repo
end
```

## Usage

### Creating a Document

```elixir
{:ok, doc} = Spector.insert(MyApp.TypedDoc, %{content: "First draft"})
assert doc.content == "First draft"
assert doc.version == 1
```

### Revising with a Supersedes Link

When revising, pass the `:supersedes` attribute to create a link to the original event:

```elixir
# Get the original event ID (same as doc.id for insert events)
original_event_id = doc.id

{:ok, revised} = Spector.execute(doc, :revise, %{
  content: "Second draft",
  supersedes: original_event_id
})
assert revised.content == "Second draft"
assert revised.version == 2
```

### Querying Links

Find which events supersede a given event:

```elixir
superseding = MyApp.TypedDoc.get_superseding_events(original_event_id)
assert length(superseding) == 1
assert hd(superseding).action == :revise
```

Find what an event supersedes:

```elixir
import Ecto.Query

# Get the revise event ID from the events table
[revise_event] = MyApp.Repo.all(
  from e in MyApp.TypedLinkEvents,
    where: e.parent_id == ^doc.id and e.action == :revise
)

superseded = MyApp.TypedDoc.get_superseded_by(revise_event.id)
assert length(superseded) == 1
assert hd(superseded).id == original_event_id
```

### Preloading Links

Events with links can be preloaded through the association:

```elixir
events = MyApp.Repo.all(MyApp.TypedLinkEvents)
  |> MyApp.Repo.preload(:links)

# The revise event should have one link
revise_event = Enum.find(events, &(&1.action == :revise))
assert length(revise_event.links) == 1
assert hd(revise_event.links).type == :supersedes
```

## Link Types Reference

Common relationship types and when to use them:

| Type | Use Case | Example |
|------|----------|---------|
| `:ancestor` | Tree-structured history | Conversation branches, version trees |
| `:parent` | Hierarchical relationships | Comment replies, nested items |
| `:sibling` | Peer relationships | Related documents, alternatives |
| `:reference` | Loose connections | Citations, mentions |
| `:supersedes` | Replacement relationships | Edits, corrections, updates |
| `:depends_on` | Dependency tracking | Build steps, prerequisites |

## Best Practices

1. **Links must connect events with the same parent_id**: Links are designed for relating events within the same record (e.g., linking messages in the same conversation, or revisions of the same document). Do not use links to connect events across different records.

2. **Choose between typed and separate link tables**: Separate link tables (one per relationship type) have a unique index on `(event_id, foreign_key)`. Typed link tables have a unique index on `(event_id, foreign_key, type)`, allowing the same pair of events to have multiple relationships of different types. Typed tables are particularly useful for heterogeneous event tables where you want flexible relationship types without creating a new link table for each one.

3. **Define clear type semantics**: Document what each type means in your domain and enforce it in `prepare_event/3`.

4. **Index appropriately**: The migration creates indexes on `event_id` and the foreign key. For typed tables, consider adding a composite index if you frequently query by type.

5. **Keep links immutable**: Links are created when events are inserted and shouldn't be modified afterward. This preserves the integrity of your event log.

6. **Use Ecto.Enum for types**: This provides compile-time checking and clear documentation of valid types.

## See Also

- [Building an AI Chat Log](AI_chat.md) - Uses links for conversation branching
- [Building a Basic Chat](basic_chat.md) - Uses links for edit history