> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erdo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dataset revisions

> A file dataset keeps the versions it overwrote — list them, query one, and recover rows a bad refresh dropped

# Dataset revisions

Erdo stores a file dataset's contents as a file. Every write that replaces those
contents — a refresh, a re-import, an upload that overwrites — stores the new file
alongside the one it replaced rather than on top of it, so the previous version is still
there after the new one goes live. A **revision** is one of those stored versions.

This matters on the day a refresh goes wrong. If a source briefly returned fewer rows,
or a rebuild ran in replace mode when it should have merged, the live dataset now holds
less than it did an hour ago — and the hour-ago version is sitting in storage, intact.
Revisions are how you look at it.

Revisions are **read-only**. Nothing here rolls a dataset back, and there is no "restore"
button: you read the rows out of the last-good revision and write them in again through
the normal write path, so the write carries provenance, honours the dataset's upsert key,
and merges rather than clobbering whatever arrived since.

## What a revision is, and is not

A revision exists for each version of a file dataset's stored contents. Alongside them
you may also see a file that was **uploaded into** an existing dataset and merged into
it — it is stored in its own right, so it is listed and queryable, but it was never the
dataset's contents. That distinction is visible: a version that was live and has since
been replaced reports **when** it was replaced; one that was never live reports nothing.

Row-level writes — appending a row, updating one, deleting one — do not create a
revision. They change the current contents in place. Revisions record whole-contents
replacements, which is the failure they exist for.

**Datasets queried live at their source keep no revisions here.** A dataset backed by a
connected database or warehouse is read from that system on every query, so Erdo holds no
copy to keep versions of, and its revision list is empty.

## Listing revisions

```bash theme={null}
erdo datasets revisions acme.leads
```

The live version comes first, then superseded ones newest first. Each row carries the
revision's **id** — the handle for reading it — when it was created, when it was
superseded (blank on the live one, and on a version that was never live), the stored
file's name, and its type. `--json` prints the raw result instead of a table.

## Reading one

Pass a revision id to the normal read. The same DuckDB SQL you would run against the
live table runs against that version's contents, with the file exposed as a table named
`data`:

```bash theme={null}
erdo datasets fetch acme.leads --revision <id> \
  --sql "SELECT * FROM data"
```

So the question "which rows did the bad refresh drop?" is a query, not an
archaeology project:

```bash theme={null}
# How many rows did the previous version hold?
erdo datasets fetch acme.leads --revision <id> --sql "SELECT count(*) FROM data"

# The rows that version had and the live one does not, by email
erdo datasets fetch acme.leads --revision <id> \
  --sql "SELECT * FROM data WHERE email NOT IN ('a@example.com','b@example.com')"
```

Reading a revision does not change the dataset. To put the rows back, take what the
query returned and write it in the ordinary way — `erdo_write_rows`, `POST
/v1/datasets/:datasetSlug/rows`, or just asking an agent — so the rows land with their
provenance and the dataset's key decides what is an insert and what is an update.

## In chat

An agent working on a dataset can do all of this itself. Ask it what a dataset held
before a refresh, or to recover rows a rebuild dropped, and it will list the revisions,
query the last-good one, and write the missing rows back.

| Tool                     | Description                                                      |
| ------------------------ | ---------------------------------------------------------------- |
| `list_dataset_revisions` | List a dataset's stored versions, live first then newest first.  |
| `query_dataset_revision` | Run SQL against one revision's contents, the table named `data`. |

## MCP tools

| Tool                          | Description                                                                                                                                                                         |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `erdo_list_dataset_revisions` | List a dataset's stored revisions (`dataset_slug`). Returns each revision's id, created and superseded timestamps, whether it is the live one, and the stored file's name and type. |
| `erdo_fetch_dataset_contents` | Read rows; pass `revision_id` to read that stored version instead of the live contents.                                                                                             |

## REST

| MCP tool                      | REST endpoint                         | Method                    |
| ----------------------------- | ------------------------------------- | ------------------------- |
| `erdo_list_dataset_revisions` | `/v1/datasets/:datasetSlug/revisions` | GET                       |
| `erdo_fetch_dataset_contents` | `/v1/datasets/:datasetSlug/fetch`     | POST (send `revision_id`) |

```bash theme={null}
# List the stored versions
curl "https://api.erdo.ai/v1/datasets/leads/revisions" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Query one of them
curl -X POST https://api.erdo.ai/v1/datasets/leads/fetch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "revision_id": "REVISION_ID",
        "sql_query": "SELECT * FROM data",
        "limit": 1000
      }'
```

Both reads need the same view access as reading the dataset itself — a revision is that
dataset's data, gated the same way. A [scoped key](/api/scoped-keys) with the
`datasets:query` capability for the dataset can make both calls.

A revision id that does not belong to the dataset you name is rejected as not found, so
an id alone never reaches another dataset's contents.
