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>
57 lines
2.4 KiB
Python
57 lines
2.4 KiB
Python
"""drop users.password_hash — D13 / T10.3
|
|
|
|
Authentication moved to an LDAPS simple bind against the domain (see
|
|
server/ldap_auth.py), so the suite no longer holds a credential of any kind.
|
|
|
|
THIS MIGRATION DESTROYS DATA AND CANNOT BE UNDONE IN ANY MEANINGFUL SENSE.
|
|
`downgrade()` recreates the column, but every hash in it is gone — and even a
|
|
restored hash would be useless, because nothing reads the column any more. The
|
|
downgrade exists so the revision is well-formed and so an operator can step back
|
|
past it, not because stepping back restores the old login. To actually revert to
|
|
local passwords you have to revert the application code and reset every password
|
|
by hand.
|
|
|
|
The column is recreated NULLABLE on downgrade, deliberately. The baseline schema
|
|
declared it NOT NULL, but there are no values to put back, so a NOT NULL column
|
|
with no server default would refuse to add itself on any table that has rows.
|
|
|
|
DO NOT WRAP THIS IN batch_alter_table. An earlier version of this migration did,
|
|
and it silently deleted every row of `project_members` on SQLite.
|
|
|
|
Why: alembic's batch mode emulates ALTER on SQLite by rebuilding the table —
|
|
create a new one, copy the rows, DROP the original, rename. `server/alembic/env.py`
|
|
imports the engine from `server/db.py`, which registers a `connect` listener setting
|
|
`PRAGMA foreign_keys=ON`, so that DROP TABLE cascades through
|
|
`project_members.user_id`, which is declared `ondelete="CASCADE"`. Every project
|
|
membership in the database goes with it, with no error and nothing in the log.
|
|
|
|
Turning the pragma off around the batch is not a fix either: `PRAGMA foreign_keys`
|
|
is a no-op inside a transaction, and alembic runs migrations in one.
|
|
|
|
The real answer is that the rebuild is unnecessary. SQLite gained native
|
|
ALTER TABLE ... DROP COLUMN in 3.35 (2021); this runtime has 3.42 and Postgres has
|
|
always had it. A plain drop_column touches one table and cascades nowhere.
|
|
|
|
Revision ID: b7e4f1a20c93
|
|
Revises: a1b8c6d4e2f9
|
|
Create Date: 2026-08-21
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = 'b7e4f1a20c93'
|
|
down_revision = 'a1b8c6d4e2f9'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
# Plain, un-batched, on both engines. See the docstring: batching this destroys
|
|
# project_members on SQLite.
|
|
op.drop_column('users', 'password_hash')
|
|
|
|
|
|
def downgrade():
|
|
op.add_column('users', sa.Column('password_hash', sa.String(length=200),
|
|
nullable=True))
|