Stabilize staff concurrency and premium emoji; enable verified Drone deployment
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -30,7 +30,7 @@ target_metadata = Base.metadata
|
||||
from src.core.database import DATABASE_URL
|
||||
|
||||
# Обновляем URL базы данных из переменных окружения
|
||||
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
||||
config.set_main_option("sqlalchemy.url", DATABASE_URL.replace("%", "%%"))
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
@@ -91,4 +91,4 @@ def run_migrations_online() -> None:
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
run_migrations_online()
|
||||
|
||||
@@ -17,72 +17,12 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('blocked_users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('telegram_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('error_type', sa.String(length=100), nullable=False),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('first_blocked_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('attempt_count', sa.Integer(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_blocked_users_is_active'), 'blocked_users', ['is_active'], unique=False)
|
||||
op.create_index(op.f('ix_blocked_users_telegram_id'), 'blocked_users', ['telegram_id'], unique=True)
|
||||
op.create_table('broadcast_channels',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('chat_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('chat_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('title', sa.String(length=255), nullable=False),
|
||||
sa.Column('username', sa.String(length=255), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('added_by', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['added_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_broadcast_channels_chat_id'), 'broadcast_channels', ['chat_id'], unique=True)
|
||||
op.create_index(op.f('ix_broadcast_channels_is_active'), 'broadcast_channels', ['is_active'], unique=False)
|
||||
op.create_table('broadcast_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('broadcast_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('target_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('message_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('message_text', sa.Text(), nullable=True),
|
||||
sa.Column('file_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('total_recipients', sa.Integer(), nullable=True),
|
||||
sa.Column('success_count', sa.Integer(), nullable=True),
|
||||
sa.Column('failed_count', sa.Integer(), nullable=True),
|
||||
sa.Column('blocked_count', sa.Integer(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_broadcast_logs_broadcast_type'), 'broadcast_logs', ['broadcast_type'], unique=False)
|
||||
op.create_index(op.f('ix_broadcast_logs_status'), 'broadcast_logs', ['status'], unique=False)
|
||||
op.add_column('users', sa.Column('is_chat_banned', sa.Boolean(), nullable=True))
|
||||
op.add_column('users', sa.Column('last_activity', sa.DateTime(timezone=True), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
# All objects in this autogenerated revision were already introduced by
|
||||
# 71376bb89294, 1f1631301809, and b4c435a7dc5f. Keep the revision marker
|
||||
# for deployed databases without creating the same objects twice.
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'last_activity')
|
||||
op.drop_column('users', 'is_chat_banned')
|
||||
op.drop_index(op.f('ix_broadcast_logs_status'), table_name='broadcast_logs')
|
||||
op.drop_index(op.f('ix_broadcast_logs_broadcast_type'), table_name='broadcast_logs')
|
||||
op.drop_table('broadcast_logs')
|
||||
op.drop_index(op.f('ix_broadcast_channels_is_active'), table_name='broadcast_channels')
|
||||
op.drop_index(op.f('ix_broadcast_channels_chat_id'), table_name='broadcast_channels')
|
||||
op.drop_table('broadcast_channels')
|
||||
op.drop_index(op.f('ix_blocked_users_telegram_id'), table_name='blocked_users')
|
||||
op.drop_index(op.f('ix_blocked_users_is_active'), table_name='blocked_users')
|
||||
op.drop_table('blocked_users')
|
||||
# ### end Alembic commands ###
|
||||
# The preceding revisions own these objects.
|
||||
pass
|
||||
|
||||
50
migrations/versions/20260701_0001_add_performance_indexes.py
Normal file
50
migrations/versions/20260701_0001_add_performance_indexes.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""add performance indexes
|
||||
|
||||
Revision ID: 20260701_perf_indexes
|
||||
Revises: 20260307_0100_add_emoji_mappings
|
||||
Create Date: 2026-07-01 00:01:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = '20260701_perf_indexes'
|
||||
down_revision = '20260307_0100_add_emoji_mappings'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_users_registered_activity "
|
||||
"ON users (is_registered, last_activity)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_participations_lottery_account "
|
||||
"ON participations (lottery_id, account_number)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_participations_user_created "
|
||||
"ON participations (user_id, created_at)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_winners_lottery_place "
|
||||
"ON winners (lottery_id, place)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_winners_user_id "
|
||||
"ON winners (user_id)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_blocked_users_lookup "
|
||||
"ON blocked_users (telegram_id, error_type, is_active)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_blocked_users_lookup")
|
||||
op.execute("DROP INDEX IF EXISTS ix_winners_user_id")
|
||||
op.execute("DROP INDEX IF EXISTS ix_winners_lottery_place")
|
||||
op.execute("DROP INDEX IF EXISTS ix_participations_user_created")
|
||||
op.execute("DROP INDEX IF EXISTS ix_participations_lottery_account")
|
||||
op.execute("DROP INDEX IF EXISTS ix_users_registered_activity")
|
||||
40
migrations/versions/20260913_staff_concurrency.py
Normal file
40
migrations/versions/20260913_staff_concurrency.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Merge legacy heads, add cashiers, prevent duplicate entries and prize places."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "20260913_staff_concurrency"
|
||||
down_revision = ("20260701_perf_indexes", "41aae82e631b")
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
connection = op.get_bind()
|
||||
# Stop safely on ambiguous historical results; never silently delete prizes.
|
||||
checks = (
|
||||
("participations", "lottery_id, account_number", "account_number IS NOT NULL"),
|
||||
("participations", "lottery_id, user_id", "account_number IS NULL AND user_id IS NOT NULL"),
|
||||
("winners", "lottery_id, place", "1=1"),
|
||||
)
|
||||
for table, columns, predicate in checks:
|
||||
duplicate = connection.execute(sa.text(
|
||||
f"SELECT 1 FROM {table} WHERE {predicate} GROUP BY {columns} HAVING COUNT(*) > 1 LIMIT 1"
|
||||
)).first()
|
||||
if duplicate:
|
||||
raise RuntimeError(f"Duplicate {table} ({columns}); review duplicates before migrating. No data removed.")
|
||||
op.add_column("users", sa.Column("is_cashier", sa.Boolean(), server_default=sa.false(), nullable=False))
|
||||
with op.batch_alter_table("participations") as batch:
|
||||
batch.create_unique_constraint("uq_participation_account", ["lottery_id", "account_number"])
|
||||
op.create_index("uq_participation_user", "participations", ["lottery_id", "user_id"], unique=True,
|
||||
postgresql_where=sa.text("account_number IS NULL"), sqlite_where=sa.text("account_number IS NULL"))
|
||||
with op.batch_alter_table("winners") as batch:
|
||||
batch.create_unique_constraint("uq_winner_place", ["lottery_id", "place"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("winners") as batch:
|
||||
batch.drop_constraint("uq_winner_place", type_="unique")
|
||||
op.drop_index("uq_participation_user", table_name="participations")
|
||||
with op.batch_alter_table("participations") as batch:
|
||||
batch.drop_constraint("uq_participation_account", type_="unique")
|
||||
op.drop_column("users", "is_cashier")
|
||||
Reference in New Issue
Block a user