The Librarr 2.0 backfill engine migrates legacy library_items rows into the
normalized book-centric schema.
It is implemented as reusable library code, not as a CLI command or API handler. The backfill engine itself does not switch service, repository, API, UI, OPDS, or import behavior; Repository Switch and Import Engine selection are separate startup/runtime decisions.
future CLI / UI wizard / upgrade task
↓
LibraryMigrationEngine
↓
LegacyStore + StateStore
↓
Normalized repository interfaces
↓
books / editions / contributors / files / identifiers / covers
The engine uses:
LegacyStoreto read legacy rows deterministically.StateStoreto record runs, row state, and migration-map checkpoints.Repositoryto write normalized domain records.
The engine does not write normalized tables with direct SQL. Normalized writes go through repository methods. SQL is isolated in the store implementation for legacy reads, run tracking, migration-map updates, and validation snapshots.
Rows are processed by ascending library_items.id.
For each row:
- Check
library_item_migration_map. - Reuse a completed mapping when present.
- Find or create a book by title/media type.
- Find or create an edition under that book.
- Attach the author as an edition contributor when available.
- Reuse an existing file by path, source ID, or content hash.
- Attach a new file when no match exists.
- Preserve legacy metadata in
files.embedded_metadata_json. - Preserve
sourceandsource_idon the file. - Add a book identifier for stable non-empty source/source ID pairs.
- Save the migration map and backfill state.
Each real row migration runs through the normalized repository transaction boundary. If the process stops before a mapping is recorded, rerunning the engine reuses normalized records by path/source/hash and then records the map.
The additive migration librarr_2_backfill_state creates:
backfill_runsbackfill_state
backfill_runs records:
- version
- start/completion timestamps
- status
- dry-run flag
- processed/migrated/skipped/error counts
- resume checkpoint
- structured report JSON
backfill_state records per-legacy-row progress:
- legacy item ID
- run ID
- status
- normalized book/edition/file IDs
- sanitized error text
library_item_migration_map remains the durable mapping used for idempotency
and future compatibility validation.
DryRun() performs legacy reads, normalized lookups, planning, and validation
without writing:
- no normalized records
- no migration-map rows
- no backfill-run rows
- no backfill-state rows
Dry-run reports predicted creates/reuses and warnings such as invalid metadata JSON.
Validate() uses a database snapshot to check:
- every legacy item has a completed mapping
- mapped books exist
- mapped editions exist
- mapped files exist
- files are attached to editions
- duplicate normalized file paths are absent
- duplicate normalized identifiers are absent
Validation produces a structured result with errors, warnings, and snapshot counts. Dry-run can report warnings without requiring completed mappings.
Resume(runID) reruns the same migration path. Existing completed
library_item_migration_map rows are reused. Rows without completed mappings
are processed again and are expected to reuse any normalized records already
created before interruption.
This makes interruption safe:
- completed rows are skipped/reused
- partially written normalized rows are reused by lookup
- missing map rows are recreated
- failed rows can be retried
The engine returns a structured Report and a human-readable Summary().
The report includes:
- legacy rows total
- rows processed/completed/skipped/failed
- books created/reused
- editions created/reused
- files created/reused
- contributors created/reused
- identifiers created
- covers created
- duplicates merged
- warnings
- errors
- elapsed time
- per-row results
- validation result
This first working engine intentionally stays conservative.
- It does not parse free-form author strings into multiple contributors.
- It does not create series unless future structured metadata support is added.
- It does not create covers because legacy rows do not have dedicated cover fields.
- It treats unknown source/source ID pairs as file provenance and only adds a simple book identifier for non-empty pairs.
- It does not switch production reads.
Recommended order:
- Run dry-run on copied databases.
- Review reports for missing files, invalid metadata, and ambiguous matches.
- Run the real engine behind a CLI/admin action.
- Validate normalized totals and relationships.
- Select normalized repository mode only after validation passes.
- Use the v2 import engine only after normalized write idempotency has live validation.