"""Add account_id to tree_shares and backfill via tree owner's account. The share belongs to the tree's tenant, not the actor who created it. A super admin in account A can share a tree owned by account B; that share must land in account B so account B's RLS filter sees it. Revision ID: a05e1a1bea7c Revises: 2a9056eddd90 Create Date: 2026-04-11 00:00:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = 'a05e1a1bea7c' down_revision: Union[str, None] = '2a9056eddd90' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.add_column('tree_shares', sa.Column('account_id', sa.UUID(), nullable=True)) op.create_foreign_key( 'fk_tree_shares_account_id', 'tree_shares', 'accounts', ['account_id'], ['id'], ondelete='CASCADE', ) # Backfill: derive from the tree's account, not the creator's account. # A share lives in the same tenant as its tree so that the tree owner's # RLS context covers their own shares regardless of who created them. op.execute(""" UPDATE tree_shares ts SET account_id = t.account_id FROM trees t WHERE ts.tree_id = t.id AND t.account_id IS NOT NULL AND ts.account_id IS NULL """) result = op.get_bind().execute( sa.text("SELECT COUNT(*) FROM tree_shares WHERE account_id IS NULL") ) count = result.scalar() if count > 0: raise RuntimeError( f"ROLLBACK: {count} tree_shares rows have NULL account_id after backfill. " "All share entries must have a creating user with an account." ) op.alter_column('tree_shares', 'account_id', nullable=False) op.create_index('ix_tree_shares_account_id', 'tree_shares', ['account_id']) def downgrade() -> None: op.drop_index('ix_tree_shares_account_id', table_name='tree_shares') op.drop_constraint('fk_tree_shares_account_id', 'tree_shares', type_='foreignkey') op.drop_column('tree_shares', 'account_id')