db-migration
GitHub指导数据库模型变更与 Alembic 迁移流程,涵盖 SQLModel DAO 规范、多方言(Postgres/SQLite)启动策略及向后兼容原则。
Trigger Scenarios
Install
npx skills add lncrawl/lightnovel-crawler --skill db-migration -g -y
SKILL.md
Frontmatter
{
"name": "db-migration",
"description": "Schema changes in lncrawl — SQLModel DAO models, Alembic migration workflow (dev migrate CLI), Postgres\/SQLite dialect differences, enum-sync migrations. Use when adding\/changing a DAO model field or writing\/reviewing a migration."
}
Schema changes
ORM: SQLModel/SQLAlchemy. Models in lncrawl/dao/, migrations in
lncrawl/migrations/versions/. ctx.db.bootstrap() runs on startup and evolves the schema
differently per dialect (services/db.py):
- Fresh DB (any dialect): built directly from the models with
create_all()+stamp head— the migration history is never replayed on a new install. - Existing SQLite (single-user CLI/desktop): migration scripts are never executed at
runtime. A fingerprint of the models is cached in
PRAGMA user_version; a matching fingerprint makes startup a single PRAGMA read (no Alembic import). On a mismatch the schema is reconciled additively — create missing tables/columns/indexes, skip drops and type changes — so an older app runs safely against a newer DB and no migration can corrupt data. - Existing Postgres/MySQL (server): real Alembic migrations (
upgrade head); never auto-downgrades when the DB is ahead of the code.
Migration scripts still matter — they are the mechanism for the server and the correctness
gate in CI (dev migrate verify replays every script from base and strict-checks against the
models). SQLite just doesn't consume them at runtime. Always generate a script for a model
change; CI fails otherwise.
Additive-first discipline (what keeps SQLite self-healing): prefer backward-compatible
model changes — new columns must be nullable or have a server_default (SQLite adds them
NULLable regardless). Renames, type narrowing, splits, and backfills are non-additive: they
apply on the server via the script, but on SQLite degrade to additive (the old column lingers,
data is not migrated). For a genuinely destructive change on the single-user path, use
dev migrate rebuild: it drops and recreates every table from the models and copies rows back
at the raw driver level (keeping only columns that still exist, preserving JSON extra
verbatim), backing up and restoring on failure. SQLite only; the server uses migration scripts.
DAO model conventions
- Every table extends
BaseTable(dao/_base.py): UUID stringidPK,created_at/updated_atas UNIX-msBigInteger(auto-touched by abefore_updateevent), and a JSONextradict. Settable=True+__tablename__; composite indexes via__table_args__. - Enum columns are stored as plain scalars, never native DB enums. IntEnum columns use
sa_type=IntEnumType(SomeEnum)(dao/_enum.py) →SMALLINTholding the member value (correct numeric ordering on every dialect; reads return the enum member and tolerate legacy name strings). String-enum columns usesa_type=sa.Enum(SomeEnum, native_enum=False)→VARCHARholding the member name. Because there is no nativeENUMtype, adding an enum member needs no migration at all — no moresync_*revisions. Enums live inlncrawl/enums.pyand are re-exported fromdao/__init__.py, which also maintains themodels/tableslists Alembic metadata uses. - Use
sa_type=sa.BigIntegerfor large ints,index=Truefor queried fields, and aserver_defaultwhen adding a NOT NULL column to an existing table.
Recipe
- Edit the DAO model in
lncrawl/dao/*.py. - Generate a revision:
uv run python -m lncrawl dev migrate add "message"(autogenerate by default;-n/--no-autofor a hand-written one). Files land inmigrations/versions/with a timestamp filename template and are auto-formatted by ablackpost-write hook — the Alembic config is built in code (services/db.py), notalembic.ini. - Review the generated
upgrade()/downgrade()— keep them symmetric (add_column+create_index↔drop_index+drop_column). House style: module docstring with Revision ID/Revises/Create Date,revision/down_revisionconstants, and adialect = op.get_context().dialect.nameguard when behavior differs per dialect. - Adding a member to an existing enum needs no migration — enum columns are plain
SMALLINT/VARCHAR(see the enum bullet above), so new members just work. The legacysync_*revisions and2026_07_14_*_drop_native_enumsare the historical record of removing the native types; don't add new enum-sync migrations. - Verify:
uv run python -m lncrawl dev migrate verify(upgrades to head + strict schema check — this is also the CI gate). Local apply/rollback/status:dev migrate up/dev migrate down/dev migrate status.
Dialect notes
- SQLite is the default (
sqlite.dbin the data dir);DATABASE_URLswitches to Postgres/MySQL.env.pyenablesrender_as_batchonly on SQLite (table-rebuild for ALTER/DROP support) — write normal Alembic ops and let batch mode handle it. - Session pattern in services:
with ctx.db.session() as sess:—expire_on_commit=Falseand no auto-commit; every writer callssess.commit()explicitly.
Version History
- b76d44a Current 2026-07-25 09:00


