Files
xiaoxia-saas/packages/adapters/sqlalchemy_impl/session.py
T
xiaoxia e989ca3a59
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m41s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m7s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m9s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m13s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m35s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m49s
AI Code Review / AI Code Review (pull_request) Successful in 7m1s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 10m22s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 15m18s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 8m8s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 42s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m17s
fix(viral-video): #2122 三阶段 pipeline 补齐 voice_id/voice_source/video_ratio 字段
- domain/model/schema 新增 voice_id/voice_source/video_ratio 三个字段
- 幂等 ALTER TABLE 迁移补列(voice_id VARCHAR(200) / voice_source VARCHAR(20) / video_ratio VARCHAR(10))
- POST /analyze-images 接收音色/比例参数,创建 job 时持久化
- POST /{id}/generate-copy 接收 voice_id/voice_source/video_ratio/style_guide,写回 job
- _step_tts 优先使用 job.voice_id,空则回退 job.persona_id,再空用默认 longxiaochun_v3
- _run_render_pipeline 调用 call_video_generation 时使用 job.video_ratio(默认 9:16)
- GET /{id} 返回 voice_id/voice_source/video_ratio
- 前端 TS types 同步补字段(AnalyzeImagesRequest/GenerateCopyRequest/ViralVideoJob)
- 新增 2 个单测验证字段持久化;viral 全量 95 个单测通过
2026-10-01 12:49:33 +08:00

139 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from sqlalchemy import create_engine, text
from sqlalchemy.engine import URL, make_url
from sqlalchemy.orm import sessionmaker
from packages.adapters.sqlalchemy_impl.models import Base
SCHEMA_INIT_LOCK_ID = 2026061501
SessionLocal = None
def build_engine(
database_url: str,
*,
pool_size: int = 20,
max_overflow: int = 40,
pool_timeout: int = 30,
pool_recycle: int = 3600,
):
# SQLite 不支持 QueuePool 的 pool_size/max_overflow/pool_timeout,
# 传了会在 create_engine 阶段直接 TypeError,这里只对非 SQLite 传连接池参数。
if _is_sqlite(database_url):
return create_engine(database_url, pool_recycle=pool_recycle)
return create_engine(
database_url,
pool_size=pool_size,
max_overflow=max_overflow,
pool_timeout=pool_timeout,
pool_recycle=pool_recycle,
)
def build_session_factory(
database_url: str,
*,
pool_size: int = 20,
max_overflow: int = 40,
pool_timeout: int = 30,
pool_recycle: int = 3600,
):
engine = build_engine(
database_url,
pool_size=pool_size,
max_overflow=max_overflow,
pool_timeout=pool_timeout,
pool_recycle=pool_recycle,
)
session_factory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
global SessionLocal
SessionLocal = session_factory
return engine, session_factory
def _is_sqlite(database_url: str) -> bool:
"""检测是否为 SQLite 数据库 URL."""
return database_url.startswith("sqlite")
def _build_admin_url(database_url: str) -> URL:
url = make_url(database_url)
return url.set(database="postgres")
def ensure_database_exists(database_url: str) -> None:
"""确保数据库存在(仅 PostgreSQL 需要,SQLite 自动创建)."""
if _is_sqlite(database_url):
return
target_url = make_url(database_url)
admin_engine = create_engine(_build_admin_url(database_url), isolation_level="AUTOCOMMIT")
try:
with admin_engine.connect() as connection:
exists = connection.execute(
text("SELECT 1 FROM pg_database WHERE datname = :database_name"),
{"database_name": target_url.database},
).scalar()
if exists:
return
connection.execute(text(f'CREATE DATABASE "{target_url.database}"'))
finally:
admin_engine.dispose()
_VIRAL_VIDEO_BACKFILL_COLS = [
("storyboard", "JSON"),
("generated_copy_text", "TEXT NOT NULL DEFAULT ''"),
("voice_id", "VARCHAR(200) NOT NULL DEFAULT ''"),
("voice_source", "VARCHAR(20) NOT NULL DEFAULT ''"),
("video_ratio", "VARCHAR(10) NOT NULL DEFAULT '9:16'"),
("video_model", "VARCHAR(100) NOT NULL DEFAULT ''"),
]
def _ensure_viral_video_columns(connection) -> None:
"""Idempotently add new columns to viral_video_jobs; create_all will not ALTER existing tables."""
from sqlalchemy import inspect as _inspect
try:
insp = _inspect(connection)
if not insp.has_table("viral_video_jobs"):
return
existing = {c["name"] for c in insp.get_columns("viral_video_jobs")}
except Exception:
return
import logging as _logging
_log = _logging.getLogger(__name__)
for col, ddl in _VIRAL_VIDEO_BACKFILL_COLS:
if col in existing:
continue
try:
connection.execute(text(f"ALTER TABLE viral_video_jobs ADD COLUMN {col} {ddl}"))
_log.info("added column viral_video_jobs.%s", col)
except Exception as e:
_log.warning("add column %s failed: %s", col, e)
def initialize_database(engine) -> None:
"""初始化数据库 schema。
PostgreSQL 使用 advisory lock 防止并发初始化冲突;
SQLite 直接 create_all(单文件,无并发风险)。
"""
if _is_sqlite(str(engine.url)):
Base.metadata.create_all(bind=engine)
return
with engine.connect() as connection:
connection.execute(text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": SCHEMA_INIT_LOCK_ID})
try:
Base.metadata.create_all(bind=connection)
connection.commit()
finally:
connection.execute(
text("SELECT pg_advisory_unlock(:lock_id)"),
{"lock_id": SCHEMA_INIT_LOCK_ID},
)
_ensure_viral_video_columns(connection)
connection.commit()