111 lines
6.2 KiB
Python
111 lines
6.2 KiB
Python
"""啟動時資料庫 schema 自動修復。
|
||
|
||
從 app.py 抽出的 repair_database_schema()。
|
||
歷史背景:早期 V9.53 起為了讓部署不必執行 migration,在啟動時動態檢查欄位並
|
||
ALTER TABLE 補上。長期應改走 Alembic / Migration script,但這個 fallback 還活著。
|
||
|
||
呼叫端:app.py 啟動末段呼叫一次。
|
||
"""
|
||
from utils.logger_manager import SystemLogger
|
||
|
||
_log = SystemLogger("SchemaRepair").get_logger()
|
||
|
||
|
||
def _ensure_column(engine, text_fn, table, column, ddl, post_sql=None):
|
||
"""補上單一欄位 helper:若不存在則 ALTER TABLE。"""
|
||
from sqlalchemy import inspect
|
||
inspector = inspect(engine)
|
||
if table not in inspector.get_table_names():
|
||
return False
|
||
columns = [c['name'] for c in inspector.get_columns(table)]
|
||
if column in columns:
|
||
return False
|
||
_log.warning(f"[Database] [Schema] ⚠️ 偵測到 {table} 表缺少 {column} 欄位 | 正在自動修復...")
|
||
with engine.connect() as conn:
|
||
conn.execute(text_fn(ddl))
|
||
if post_sql:
|
||
conn.execute(text_fn(post_sql))
|
||
conn.commit()
|
||
_log.info(f"[Database] [Schema] ✅ {table}.{column} 欄位修復完成")
|
||
return True
|
||
|
||
|
||
def repair_database_schema():
|
||
"""啟動時資料庫結構自動修復(V9.53)。"""
|
||
from database.manager import DatabaseManager
|
||
from sqlalchemy import inspect, text
|
||
from config import DATABASE_TYPE
|
||
|
||
db = DatabaseManager()
|
||
engine = db.engine
|
||
|
||
try:
|
||
# V9.96: SQLite WAL 模式(解決 database is locked)
|
||
if DATABASE_TYPE == 'sqlite':
|
||
with engine.connect() as conn:
|
||
conn.execute(text("PRAGMA journal_mode=WAL"))
|
||
conn.commit()
|
||
_log.info("[Database] [WAL] ✅ SQLite WAL 模式已啟用 | 提升並發寫入效能")
|
||
else:
|
||
_log.info(f"[Database] ✅ 使用 {DATABASE_TYPE.upper()} 資料庫")
|
||
|
||
inspector = inspect(engine)
|
||
|
||
# V9.70: products 表
|
||
_ensure_column(engine, text, 'products', 'image_url',
|
||
"ALTER TABLE products ADD COLUMN image_url TEXT")
|
||
_ensure_column(engine, text, 'products', 'created_at',
|
||
"ALTER TABLE products ADD COLUMN created_at DATETIME",
|
||
post_sql="UPDATE products SET created_at = updated_at WHERE created_at IS NULL")
|
||
|
||
# promo_products 表
|
||
promo_columns = [
|
||
('url', "ALTER TABLE promo_products ADD COLUMN url TEXT"),
|
||
('image_url', "ALTER TABLE promo_products ADD COLUMN image_url TEXT"),
|
||
('previous_price', "ALTER TABLE promo_products ADD COLUMN previous_price INTEGER"),
|
||
('session_time_text', "ALTER TABLE promo_products ADD COLUMN session_time_text TEXT"),
|
||
('remain_qty', "ALTER TABLE promo_products ADD COLUMN remain_qty INTEGER"),
|
||
('discount_text', "ALTER TABLE promo_products ADD COLUMN discount_text TEXT"),
|
||
('page_type', "ALTER TABLE promo_products ADD COLUMN page_type TEXT DEFAULT 'edm'"),
|
||
]
|
||
for col_name, ddl in promo_columns:
|
||
_ensure_column(engine, text, 'promo_products', col_name, ddl)
|
||
|
||
# V10.328: PChome/MOMO 比價診斷欄位。正式價差與待審嘗試都需可回溯
|
||
# URL、圖片、庫存與 matcher 結構化原因,避免「待比對」無法被人工處理。
|
||
json_type = 'JSONB' if DATABASE_TYPE == 'postgresql' else 'TEXT'
|
||
competitor_price_columns = [
|
||
('competitor_product_url', "ALTER TABLE competitor_prices ADD COLUMN competitor_product_url TEXT"),
|
||
('competitor_image_url', "ALTER TABLE competitor_prices ADD COLUMN competitor_image_url TEXT"),
|
||
('competitor_stock', "ALTER TABLE competitor_prices ADD COLUMN competitor_stock INTEGER"),
|
||
('match_diagnostic_json', f"ALTER TABLE competitor_prices ADD COLUMN match_diagnostic_json {json_type}"),
|
||
('comparison_mode', "ALTER TABLE competitor_prices ADD COLUMN comparison_mode VARCHAR(40)"),
|
||
('hard_veto', "ALTER TABLE competitor_prices ADD COLUMN hard_veto BOOLEAN"),
|
||
('diagnostic_codes', f"ALTER TABLE competitor_prices ADD COLUMN diagnostic_codes {json_type}"),
|
||
]
|
||
competitor_history_columns = [
|
||
('competitor_product_url', "ALTER TABLE competitor_price_history ADD COLUMN competitor_product_url TEXT"),
|
||
('competitor_image_url', "ALTER TABLE competitor_price_history ADD COLUMN competitor_image_url TEXT"),
|
||
('competitor_stock', "ALTER TABLE competitor_price_history ADD COLUMN competitor_stock INTEGER"),
|
||
('match_diagnostic_json', f"ALTER TABLE competitor_price_history ADD COLUMN match_diagnostic_json {json_type}"),
|
||
('comparison_mode', "ALTER TABLE competitor_price_history ADD COLUMN comparison_mode VARCHAR(40)"),
|
||
('hard_veto', "ALTER TABLE competitor_price_history ADD COLUMN hard_veto BOOLEAN"),
|
||
('diagnostic_codes', f"ALTER TABLE competitor_price_history ADD COLUMN diagnostic_codes {json_type}"),
|
||
]
|
||
competitor_attempt_columns = [
|
||
('competitor_product_url', "ALTER TABLE competitor_match_attempts ADD COLUMN competitor_product_url TEXT"),
|
||
('competitor_image_url', "ALTER TABLE competitor_match_attempts ADD COLUMN competitor_image_url TEXT"),
|
||
('competitor_stock', "ALTER TABLE competitor_match_attempts ADD COLUMN competitor_stock INTEGER"),
|
||
('match_diagnostic_json', f"ALTER TABLE competitor_match_attempts ADD COLUMN match_diagnostic_json {json_type}"),
|
||
('comparison_mode', "ALTER TABLE competitor_match_attempts ADD COLUMN comparison_mode VARCHAR(40)"),
|
||
('hard_veto', "ALTER TABLE competitor_match_attempts ADD COLUMN hard_veto BOOLEAN"),
|
||
('diagnostic_codes', f"ALTER TABLE competitor_match_attempts ADD COLUMN diagnostic_codes {json_type}"),
|
||
]
|
||
for table_columns in (competitor_price_columns, competitor_history_columns, competitor_attempt_columns):
|
||
for col_name, ddl in table_columns:
|
||
table_name = ddl.split()[2]
|
||
_ensure_column(engine, text, table_name, col_name, ddl)
|
||
|
||
except Exception as e:
|
||
_log.error(f"[Database] [Schema] ❌ 資料庫修復失敗 | Error: {e}")
|