"""notification table (消息通知中心 站内消息) Revision ID: notification_table Revises: 1a924c274fce Create Date: 2026-07-15 12:00:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = 'notification_table' down_revision: Union[str, Sequence[str], None] = '1a924c274fce' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None # PG 用 JSONB,SQLite 退化为通用 JSON(与 models/notification._JSON 一致)。 _JSON = sa.JSON().with_variant(postgresql.JSONB(), 'postgresql') def upgrade() -> None: op.create_table( 'notification', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('type', sa.String(length=32), nullable=False), sa.Column('coins', sa.Integer(), nullable=True), sa.Column('cash_cents', sa.Integer(), nullable=True), sa.Column('info_rows', _JSON, nullable=False), sa.Column('extra', _JSON, nullable=False), sa.Column('is_read', sa.Boolean(), nullable=False), sa.Column('read_at', sa.DateTime(timezone=True), nullable=True), sa.Column('dedup_key', sa.String(length=64), nullable=True), sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.PrimaryKeyConstraint('id'), ) with op.batch_alter_table('notification', schema=None) as batch_op: batch_op.create_index('ix_notification_type', ['type'], unique=False) # 列表分页:按用户取 + sent_at 倒序 batch_op.create_index('ix_notification_user_sent', ['user_id', 'sent_at'], unique=False) # 铃铛角标:count where user_id=? and is_read=false —— 部分索引只覆盖未读行 batch_op.create_index( 'ix_notification_user_unread', ['user_id'], unique=False, sqlite_where=sa.text('is_read = 0'), postgresql_where=sa.text('is_read = false'), ) # 去重/合并:同一 (user, type, dedup_key) 未读期间只允许一条(已读后可再生成) batch_op.create_index( 'uq_notification_user_type_dedup', ['user_id', 'type', 'dedup_key'], unique=True, sqlite_where=sa.text('dedup_key IS NOT NULL AND is_read = 0'), postgresql_where=sa.text('dedup_key IS NOT NULL AND is_read = false'), ) def downgrade() -> None: with op.batch_alter_table('notification', schema=None) as batch_op: batch_op.drop_index('uq_notification_user_type_dedup') batch_op.drop_index('ix_notification_user_unread') batch_op.drop_index('ix_notification_user_sent') batch_op.drop_index('ix_notification_type') op.drop_table('notification')