T10.3 fix - the drop migration was deleting every project membership
Found by actually seeding the pre-migration schema and rolling forward, rather than by checking that the column disappeared. The column disappeared correctly; project_members came back empty. batch_alter_table emulates ALTER on SQLite by rebuilding the table - create a new one, copy the rows, DROP the original, rename. server/alembic/env.py:22 imports the engine from server/db.py, which registers a connect listener setting PRAGMA foreign_keys=ON, so that DROP TABLE cascaded through project_members.user_id (ondelete="CASCADE") and took every membership row with it. No error, nothing in the log, and the users table looked perfect afterwards. Production would have escaped it - Postgres does a real ALTER TABLE DROP COLUMN and touches nothing else - so this was a local-dev and test-fixture data loss, which is worse in one specific way: the tests CLAUDE.md requires run against a throwaway SQLite database, so the suite would have been validating behaviour against silently emptied membership tables. Wrapping the batch in PRAGMA foreign_keys=OFF is not the fix: that pragma is a no-op inside a transaction and alembic runs migrations in one. The rebuild is simply unnecessary - SQLite has had native ALTER TABLE DROP COLUMN since 3.35 (2021), this runtime has 3.42, and Postgres has always had it. Plain op.drop_column touches one table and cascades nowhere. The reasoning is written into the migration's docstring as a DO NOT, because batch_alter_table is the reflexive thing to reach for when a migration has to work on SQLite and the failure is invisible. Re-verified with memberships in the fixture: 3/3 users survive, roles intact (admin still admin) 2/2 project_members survive downgrade -1 -> column back, nullable; upgrade -> gone again Also closed BL-026: notify.send_now removed. Nothing referenced it and its docstring described itself entirely in terms of password resets. send_email, which it wrapped, is untouched and still used by the outbox. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,22 +121,6 @@ def send_email(s: dict, to_addr: str, subject: str, body: str) -> None:
|
||||
srv.send_message(msg)
|
||||
|
||||
|
||||
def send_now(db: Session, to_addr: str, subject: str, body: str) -> bool:
|
||||
"""Send one email immediately, outside the outbox. Used for password resets —
|
||||
a reset link must never sit in a queue, and it must not be persisted in the
|
||||
notifications table where an admin could read it and take over the account.
|
||||
Returns True if it went out."""
|
||||
s = get_settings(db)
|
||||
if not (s.get("email_enabled") and smtp_ready(s) and to_addr):
|
||||
return False
|
||||
try:
|
||||
send_email(s, to_addr, subject, body)
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001 — never surface SMTP detail to the caller
|
||||
log.warning("password-reset email to %s failed: %s", to_addr, e)
|
||||
return False
|
||||
|
||||
|
||||
def enqueue(db: Session, *, user: "models.User", kind: str, subject: str, body: str,
|
||||
link: str = "", wp_id: Optional[str] = None, project_id: Optional[str] = None) -> "models.Notification":
|
||||
"""Record a notification. Marked 'pending' only if email is enabled + SMTP ready +
|
||||
|
||||
Reference in New Issue
Block a user