103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""Client SQLite 数据库的路径、连接和初始化函数。"""
|
|
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional, Union
|
|
|
|
from .db_schema import MIGRATIONS, SCHEMA_VERSION
|
|
|
|
|
|
PathValue = Union[str, Path]
|
|
|
|
|
|
class DatabaseVersionError(RuntimeError):
|
|
"""数据库版本比当前程序支持的版本新。"""
|
|
|
|
|
|
def data_dir() -> Path:
|
|
"""返回可写数据目录;目录不存在时自动创建。"""
|
|
|
|
if getattr(sys, "frozen", False):
|
|
executable_directory = Path(sys.executable).resolve().parent
|
|
# 发布包结构是 Launcher.exe + app/CMAutoBuy.exe。主程序位于 app
|
|
# 目录时,data 必须放在上一层,升级替换 app 才不会覆盖本地数据库。
|
|
install_root = (
|
|
executable_directory.parent
|
|
if executable_directory.name.casefold() == "app"
|
|
else executable_directory
|
|
)
|
|
directory = install_root / "data"
|
|
else:
|
|
directory = Path(__file__).resolve().parents[1] / "data"
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
return directory
|
|
|
|
|
|
def app_dir() -> Path:
|
|
"""返回只读资源根目录;源码和打包运行使用同一相对结构。"""
|
|
|
|
if getattr(sys, "frozen", False):
|
|
return Path(sys._MEIPASS).resolve()
|
|
return Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def default_database_path() -> Path:
|
|
"""返回默认数据库文件路径。"""
|
|
|
|
return data_dir() / "client.db"
|
|
|
|
|
|
def open_database(db_path: Optional[PathValue] = None) -> sqlite3.Connection:
|
|
"""打开一个独立连接,并启用项目要求的 SQLite 参数。
|
|
|
|
调用者用完后必须关闭连接。不同线程不能共享同一个连接。
|
|
"""
|
|
|
|
path = Path(db_path) if db_path is not None else default_database_path()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
connection = sqlite3.connect(str(path), timeout=5.0)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys = ON")
|
|
connection.execute("PRAGMA journal_mode = WAL")
|
|
connection.execute("PRAGMA busy_timeout = 5000")
|
|
return connection
|
|
|
|
|
|
def migrate_database(connection: sqlite3.Connection) -> None:
|
|
"""把数据库从当前版本依次迁移到最新版本。"""
|
|
|
|
current_version = int(connection.execute("PRAGMA user_version").fetchone()[0])
|
|
if current_version > SCHEMA_VERSION:
|
|
raise DatabaseVersionError(
|
|
f"数据库版本 {current_version} 高于程序支持版本 {SCHEMA_VERSION}"
|
|
)
|
|
|
|
for version in range(current_version + 1, SCHEMA_VERSION + 1):
|
|
statements = MIGRATIONS.get(version)
|
|
if statements is None:
|
|
raise RuntimeError(f"缺少数据库迁移版本 {version}")
|
|
|
|
try:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
for statement in statements:
|
|
connection.execute(statement)
|
|
connection.execute(f"PRAGMA user_version = {version}")
|
|
connection.commit()
|
|
except Exception:
|
|
connection.rollback()
|
|
raise
|
|
|
|
|
|
def initialize_database(db_path: Optional[PathValue] = None) -> Path:
|
|
"""创建或升级数据库,完成后返回数据库文件路径。"""
|
|
|
|
path = Path(db_path) if db_path is not None else default_database_path()
|
|
connection = open_database(path)
|
|
try:
|
|
migrate_database(connection)
|
|
finally:
|
|
connection.close()
|
|
return path
|