# Phase 2: Data Layer — Phase Summary

## What Was Built

Phase 2 built the complete data persistence layer that all future phases write to and read from. This includes Pydantic v2 domain models for every MVP entity (Campaign, Briefing, Mission, Module, MissionChain, FileSnapshot, and four junction models), a hand-written SQLite schema with 11 tables and 16 indexes, a connection management layer that initialises the database with correct pragmas, and five repository classes that translate between Python models and database rows. The entry point is `open_campaign_db()` in `src/unfamiliar/db/__init__.py`, which opens/creates the database, runs schema initialisation, writes a `.repo_path` marker file, and returns all five repositories ready to use. No ORM was used — all queries are parameterised hand-written SQL.

## Key Decisions

**No ORM:** The plan explicitly chose `sqlite3` stdlib over SQLAlchemy. The repository layer is thin — it maps between Pydantic models and Row objects — so the overhead of an ORM wasn't justified. The trade-off is that schema and queries are hand-written SQL, but the schema is small enough that this is less complex, not more.

**TypeAdapter over model_validate:** Repositories use `TypeAdapter(Model)` instances at module level for row-to-model conversion rather than calling `Model.model_validate()` directly. This pre-compiles the validation schema and is more performant for repeated use in list queries.

**Forward-compatible v1.1 columns:** `mode` and `character` columns were added to the `campaign` table with defaults (`onboarding`, `detective`). Entry Point, Dependency, and Module-Dependency tables were intentionally excluded — they're new tables in v1.1 (purely additive, no ALTER TABLE needed).

**JSON for list/dict fields:** `category_relevance` (on Module) and `tech_stack` (on Briefing) are stored as JSON text. Serialisation helpers `_serialize_categories`/`_deserialize_categories` centralise the round-trip logic in `module.py`.

**INSERT OR REPLACE for upsert:** `FileSnapshotRepository.upsert()` uses `INSERT OR REPLACE` to handle re-analysis. SQLite's REPLACE deletes the old row and inserts a new one, which means the AUTOINCREMENT id changes on update. The snapshot is accessed by `(campaign_id, file_path)` in practice, so the changing id is not a functional problem, but it's worth knowing.

## Complex Logic Explained

**`create_many()` id retrieval pattern:** After `executemany()` inserts multiple rows, SQLite's `lastrowid` only returns the last insert. To get all ids, the repositories re-query the campaign's full entity list and match by a unique field (`path` for modules, `title_plain` for missions, `name_plain` for chains). This relies on the assumption that those fields are unique within a campaign — a constraint that exists at the application level but is not enforced by a database UNIQUE constraint on those tables.

**`init_database()` idempotency:** All schema DDL uses `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS`. The meta table row for `schema_version` uses `INSERT OR IGNORE`. Together, calling `init_database()` on an already-initialised database is a no-op.

**File permissions:** The database file is chmoded to 600 (owner-only) after creation. The path is retrieved from `PRAGMA database_list` (the connection's internal record of attached files) rather than from the input `db_path`. This handles edge cases where the path was resolved differently. The chmod is wrapped in `contextlib.suppress(OSError)` — it's a hardening measure, not a requirement for functionality.

## Connection to Previous Phases

This phase imports `constants.py` enums from Phase 1 (`CampaignStatus`, `MissionStatus`, `InvestigationCategory`, etc.) throughout the models and repositories. The `.repo_path` marker file written by `open_campaign_db()` integrates with the `_campaign_matches_repo()` function already present in Phase 1's `config.py`. Phase 3 (static analysis) and Phase 6 (LLM integration) will receive repositories via `open_campaign_db()` — the interface contracts in `plan.md` define exactly what those later phases can depend on.
