"""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))