Summary
Running make migration msg="init" against the default SQLite dev DB produces:
.../alembic/ddl/impl.py:851: UserWarning: autogenerate skipping metadata-specified
expression-based index 'ix_users_user_email_lower'; dialect 'sqlite' under
SQLAlchemy 2.0.49 can't reflect these indexes so they can't be compared
The functional index ix_users_user_email_lower is defined in the users module's User model (presumably to support func.lower(User.email) == ... lookups — which users/bootstrap.py actually does). On SQLite, autogenerate skips it entirely, so the generated migration omits it. Users developing on SQLite never have this index; users deploying on Postgres do. Same query plans look fine locally and degrade under load in prod.
Repro
smpy new myapp --yes
cd myapp
make install
make migration msg="init"
# warning appears; check generated file:
grep -i email_lower host/migrations/versions/*_init.py
# → (no match)
Suggested fix (any one)
- Hand-write the seed migration that ships with the users module so the index is always created, on every dialect.
- Wrap the index in a dialect guard — only declare on Postgres, since SQLite's
LIKE is case-insensitive by default and the index buys you less anyway.
- At minimum, document the gap — call out in the migrations docs that SQLite dev DBs will be missing this index, and warn users to verify their generated migrations on Postgres before shipping.
(1) is the cleanest; the index has a clear purpose tied to the email-lower lookup in users/bootstrap.py.
Summary
Running
make migration msg="init"against the default SQLite dev DB produces:The functional index
ix_users_user_email_loweris defined in theusersmodule'sUsermodel (presumably to supportfunc.lower(User.email) == ...lookups — whichusers/bootstrap.pyactually does). On SQLite, autogenerate skips it entirely, so the generated migration omits it. Users developing on SQLite never have this index; users deploying on Postgres do. Same query plans look fine locally and degrade under load in prod.Repro
Suggested fix (any one)
LIKEis case-insensitive by default and the index buys you less anyway.(1) is the cleanest; the index has a clear purpose tied to the email-lower lookup in
users/bootstrap.py.