# 5e-database-sqlite

D&D 5e SRD data as a single SQLite file, built from the JSON published by
[5e-bits/5e-database](https://github.com/5e-bits/5e-database).

**Currently tracking upstream release
[v5.10.0](https://github.com/5e-bits/5e-database/releases/tag/v5.10.0).**
The pin lives in `pyproject.toml` (`[tool.dnd5e-database-sqlite]`).

## Using the database

Grab `dnd5e-<edition>-<language>-<tag>.sqlite3` from the
[Releases page](https://github.com/vkama/5e-database-sqlite/releases) and
open it with any SQLite client — no server, no dependencies. (Releases are
published automatically when a `vX.Y.Z` tag matching the upstream release is
pushed; if none exists yet for the version you need, see
[Build the database](#build-the-database) below to make your own in one
command.) Every collection is a table (`monsters`, `spells`, `equipment`, …)
with three columns:

| column | meaning |
|---|---|
| `index` | unique id, primary key (`"adult-black-dragon"`) |
| `name` | display name, indexed |
| `data` | the complete record as JSON — same shape as the [dnd5eapi.co](https://www.dnd5eapi.co/) API |

```sql
-- simple lookup (uses the name index)
SELECT data FROM spells WHERE name = 'Fireball';

-- query into the JSON
SELECT name, json_extract(data, '$.challenge_rating') AS cr
FROM monsters WHERE cr >= 20 ORDER BY cr DESC;

-- expand a JSON array: every monster immune to poison
SELECT m.name
FROM monsters m, json_each(m.data, '$.damage_immunities') d
WHERE d.value = 'poison';
```

```python
import sqlite3, json

db = sqlite3.connect("dnd5e-2014-en-v5.10.0.sqlite3")
fireball = json.loads(db.execute(
    "SELECT data FROM spells WHERE name = ?", ("Fireball",)
).fetchone()[0])
print(fireball["desc"][0])
```

JSON functions require SQLite 3.38+ (or any build with the JSON1 extension —
that's virtually every runtime since 2016). The `metadata` table records which
upstream release, edition, and language the file was built from, plus license
and attribution.

### What's inside `data`

`data` is the complete record, not a subset — every field the collection's
model defines, nested objects and arrays intact, dropped keys only where the
original was absent (`null`s aren't stored). It's a byte-for-byte round trip
of the upstream JSON record: re-serializing it reproduces exactly what
upstream published, cross-references included (`school`, `classes`,
`damage.damage_type`, etc. are all `{index, name, url}` pointers you resolve
with a second query — there are no SQL foreign keys).

The shape is per-collection, not uniform: a `spells` row's `data` follows the
spell schema, a `races` row's follows the race schema. There's no single
"record" shape across tables. The canonical definition of each collection's
shape lives in this repo's pydantic models (one file per collection —
`dnd5e_db/models/<edition>/<collection>.py`), which mirror upstream's zod
schemas 1:1; each file's docstring names the exact upstream `.ts` schema it
mirrors, under [5e-bits/5e-database's `src/*/schemas/`](https://github.com/5e-bits/5e-database/tree/main/src)
— useful as a language-neutral reference if you're not working in Python.

## Installing as a library

```bash
pip install 5e-database-sqlite
```

(Not published yet as of this writing — see "Releasing" below. Until
the first tagged release goes out, install straight from git instead:
`pip install git+https://github.com/vkama/5e-database-sqlite.git`.)
`SRD.open()` and `SRD2024.open()` both work right away with no local checkout
needed — 2014/en and 2024/en databases both ship bundled inside the package.

## Python API

From Python you don't need SQL at all — every record is a typed
[pydantic](https://docs.pydantic.dev/) model with autocomplete:

```python
from dnd5e_db.srd import SRD

with SRD.open() as srd:              # 2014/en: builds from data/ in a checkout,
                                      # or opens the db bundled in the package otherwise
    aboleth = srd.monsters["aboleth"]        # by index
    aboleth.challenge_rating                 # 10.0
    aboleth.speed.swim                       # "40 ft."

    srd.spells.named("Fireball")             # by display name
    [m.name for m in srd.monsters if m.challenge_rating >= 20]
    [s.name for s in srd.spells if s.level == 3 and s.school.index == "evocation"]
```

Collections support `[]`, `.get()`, `in`, `len()`, iteration, and `.named()` —
plain Python, no query language.

### Discovering a collection's fields

`srd.spells` and `srd.races` don't hand you dicts — each item is a specific
model instance (`Spell`, `Race`), so what properties it has is discoverable
the same way as any typed Python object:

- **Autocomplete.** `srd.spells["fireball"].` in an editor/type checker lists
  `desc`, `range`, `level`, `damage`, `school`, `classes`, … — `Collection[M]`
  is generic, so `srd.spells` types as `Collection[Spell]` and `srd.races` as
  `Collection[Race]` without running anything.
- **At runtime**, `Spell.model_fields` (or `Race.model_fields`) gives you
  `{field_name: FieldInfo(type, ...)}` for every field, nested model types
  included; `Spell.model_json_schema()` expands that recursively into a full
  JSON Schema document.
- **In source**, `dnd5e_db/models/srd2014/spells.py` /
  `.../races.py` are the canonical definitions — see "What's inside `data`"
  above.

The 2024 SRD preview reads the same way through `SRD2024.open()` — identical
interface, 2024 collections (`species`/`subspecies` instead of
`races`/`subraces`, plus `poisons` and `weapon_mastery_properties`; no spells
or rules — upstream hasn't ported them yet, and monsters currently holds only
a handful of records).

## License

The D&D SRD content is © Wizards of the Coast LLC, released under
[CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/); each database file
embeds the required attribution in its `metadata` table. The upstream JSON is
maintained by [5e-bits/5e-database](https://github.com/5e-bits/5e-database)
(MIT). The build tooling in this repo is released under [MIT](LICENSE).

## Setup (building it yourself)

Requires Python 3.14. If it's not already on your system, install it locally with [uv](https://docs.astral.sh/uv/) (no sudo needed):

```bash
uv python install 3.14
poetry env use $(uv python find 3.14)
```

Then:

```bash
poetry install
```

## Build the database

The upstream JSON is already vendored under `data/` (checked into git), so a
plain build works right away — no fetch step needed:

```bash
poetry run python -m dnd5e_db.build
# -> dist/dnd5e-2014-en-v5.10.0.sqlite3

poetry run python -m dnd5e_db.build --edition 2024   # dist/dnd5e-2024-en-v5.10.0.sqlite3
```

`build` validates the vendored data against our pydantic models first and
refuses to write if anything doesn't match — a validation failure means
upstream changed shape, not that it's safe to skip.

Bumping to a newer upstream release also involves `dnd5e_db.fetch`
(re-download the vendored data for a new tag), `dnd5e_db.validate` (drift
report without building), and `dnd5e_db.check` (is the pin still upstream's
latest?) — see `CLAUDE.md` for the full workflow.

## Releasing

Pushing a `vX.Y.Z` tag matching the pinned upstream release (or a `-rN`
suffix for a tooling-only re-release of the same data) runs
`.github/workflows/release.yml`, which:

1. Runs the stable gate (tests + `validate --edition 2014`).
2. Builds the 2014/en database and attaches it to a new GitHub Release.
3. Builds the sdist/wheel with the package version set to the tag
   (PEP 440-normalized: `v5.10.0` → `5.10.0`, `v5.10.0-r2` → `5.10.0.post2`)
   and publishes to PyPI (bundling both editions' databases — see step 4).
4. A second, independent job runs the 2024 preview's own gate (`pytest -m
   srd2024` + `validate --edition 2024`), builds the 2024/en database, and
   attaches it to the same GitHub Release as a second asset. This job only
   runs after step 1-3 succeed (it needs the release to already exist to
   upload to), but never blocks them — if the 2024 preview corpus has drifted,
   only this job fails; the 2014 release and PyPI publish already went out.

`workflow_dispatch` runs the same gates and builds as a dry run (uploads
workflow artifacts) without publishing anything.

PyPI publishing uses [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
(OIDC) rather than a stored API token — nothing to generate or rotate, but it
needs a one-time setup on PyPI before the first release: on the
["Publishing" tab](https://pypi.org/manage/account/publishing/) of a PyPI
account with rights to the `5e-database-sqlite` project name (or via "pending
publisher" if the project doesn't exist on PyPI yet), register a GitHub
publisher with:

| field | value |
|---|---|
| PyPI project name | `5e-database-sqlite` |
| Owner | `vkama` |
| Repository name | `5e-database-sqlite` |
| Workflow name | `release.yml` |
| Environment name | *(leave blank)* |

## Vendoring this repo in your own build

If your project wants the built `.sqlite3` as part of *its* build (rather than
just grabbing a released file), pull this repo in as a git submodule and build
from inside it:

```bash
git submodule add https://github.com/vkama/5e-database-sqlite.git vendor/5e-database-sqlite
cd vendor/5e-database-sqlite
poetry install
poetry run python -m dnd5e_db.build --out ../../assets   # --out sends the db wherever you want it
```

`--out` is an existing flag on `dnd5e_db.build` — nothing above needs Poetry
specifically either: the only runtime dependency is pydantic, so `pip install
pydantic` plus `python -m dnd5e_db.build --out <path>` (run from the submodule
root) works in any Python 3.14 toolchain that doesn't want Poetry involved.

A submodule pins an exact commit, so picking up a newer upstream release means
`git submodule update --remote` (or checking out a new tag) plus a commit in
*your* repo — bumps stay explicit and reviewable. Note that vendoring pulls in
the full `data/` tree (~7.5 MB) as part of the clone. If you just want the file
without rebuilding anything, see "Using the database" above once releases are
published.

## Test

```bash
poetry run pytest
```
