File size: 2,995 Bytes
6e876c1
 
 
 
 
 
 
 
 
 
 
 
837a707
6e876c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837a707
 
 
 
 
 
 
 
 
 
 
 
 
384e6b6
4bc82b0
8193c6f
23938a8
837a707
 
 
 
 
 
 
 
 
 
 
6e876c1
837a707
6e876c1
837a707
6e876c1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""数据库引擎与会话管理。

engine 由 config.DATABASE_URL 决定:
- sqlite:///... 本地文件(默认)
- postgresql+psycopg://... 云端(部署时通过环境变量切换)
"""
from __future__ import annotations

import logging
from contextlib import contextmanager
from typing import Iterator

from sqlalchemy import create_engine, inspect, text
from sqlalchemy.orm import Session, sessionmaker

from .config import DATABASE_URL
from .models import Base

logger = logging.getLogger(__name__)

# SQLite 在多线程(websocket)下需要关闭线程检查
_connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}

# pool_pre_ping:serverless Postgres(如 Neon)空闲会休眠,用连接前先探活,
# 避免拿到已失效的连接导致首次请求报错。
engine = create_engine(
    DATABASE_URL, future=True, connect_args=_connect_args, pool_pre_ping=True
)
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False, future=True)


@contextmanager
def get_session() -> Iterator[Session]:
    """提供一个自动 commit / rollback / close 的会话上下文。"""
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()


def _run_light_migrations() -> None:
    """轻量迁移:给「已存在的表」补上后期新增的列(幂等)。

    create_all 只建缺失的表,不会给已有表加列;线上 Neon / 本地 SQLite 都已有
    历史数据,故这里用 inspector 检测缺列并 ALTER 补齐。区分方言以兼容布尔默认值
    (Postgres 用 false,SQLite 用 0)。新增列均带默认值,老数据零影响。
    """
    insp = inspect(engine)
    existing = set(insp.get_table_names())
    bool_false = "false" if engine.dialect.name == "postgresql" else "0"
    wanted = [
        ("users", "is_goalkeeper", f"BOOLEAN NOT NULL DEFAULT {bool_false}"),
        ("stats", "saves", "INTEGER NOT NULL DEFAULT 0"),
        ("matches", "mvp_locked", f"BOOLEAN NOT NULL DEFAULT {bool_false}"),
        ("matches", "mvp_user_id", "INTEGER"),  # 可空:None=未结算
        ("matches", "mvp2_user_id", "INTEGER"),  # 可空:双 MVP 第二人
        ("matches", "dmvp_user_id", "INTEGER"),  # 可空:铁血防守 MVP
    ]
    for table, col, ddl in wanted:
        if table not in existing:
            continue  # 新库由 create_all 直接建好(已含新列)
        if col in {c["name"] for c in insp.get_columns(table)}:
            continue
        with engine.begin() as conn:
            conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {col} {ddl}"))
        logger.info("迁移:已为 %s 表新增列 %s", table, col)


def init_db() -> None:
    """创建所有表(幂等)并执行轻量迁移补列。"""
    Base.metadata.create_all(engine)
    _run_light_migrations()
    logger.info("数据库已就绪:%s", DATABASE_URL)