fix(automation): 去掉 MySQL 不认的 NULLS FIRST,排序改用可移植写法
生产 MySQL 上手动跑第一轮就报 1064: You have an error in your SQL syntax ... near 'NULLS FIRST, login_tasks.id ASC' NULLS FIRST 是 PostgreSQL/SQLite 语法,MySQL 不支持。本地测试全绿是因为跑在 SQLite 上(它认识这个语法),属于方言差异漏网 —— 生产是 MySQL、测试是 SQLite, 行为测试覆盖不到这类问题。 改成 `ck_checked_at IS NULL DESC, ck_checked_at ASC, id ASC`:三个方言下语义 一致(没检测过的优先)且都是合法 SQL。顺带把选行查询抽成 build_select_query, 便于按方言编译做回归。 新增回归测试:用 MySQL 方言 compile() 选行 SQL,断言不含 NULLS FIRST/LAST, 且「没检测过优先」用可移植写法表达。 规划附录 C.2 记下这条:写 SQL 前先确认方言;P0 连的是 order_site 的 PostgreSQL, 项目里已经出现 MySQL / SQLite / PG 三套方言。
This commit is contained in:
@@ -1176,6 +1176,11 @@ OSS_PREFIX=collect
|
||||
P1a 踩过一次:`.env` 加了 `AUTOMATION_ENABLED=true`,容器里查不到,调度器照旧不启动;
|
||||
早先 §15.1 写入的 `ORDER_SITE_*` 与 `OSS_*` 同样没生效。新增变量时三处都要动:
|
||||
`.env`(服务器实值)+ `docker-compose.yml`(透传)+ `.env.example`(留档);
|
||||
- **注意方言差异:生产是 MySQL 8,测试是 SQLite**。SQLite 能吃下的写法 MySQL 未必认,行为测试也发现不了。
|
||||
P1a 踩过一次:`ORDER BY ck_checked_at ASC NULLS FIRST` 在 SQLite 正常,MySQL 直接报
|
||||
`1064 ... near 'NULLS FIRST'`。跨方言的排序用可移植写法(如 `col IS NULL DESC, col ASC`),
|
||||
并在测试里按 MySQL 方言 `compile()` 一次做回归。**P0 更要注意:它连的是 order_site 的 PostgreSQL**,
|
||||
三套方言(MySQL / SQLite / PG)都出现过,写 SQL 前先想清楚方言;
|
||||
- **DB 读出来的时间是 naive UTC**:`datetime` 列取回后不带时区,直接和 `datetime.now(UTC)` 比较会
|
||||
`TypeError: can't compare offset-naive and offset-aware`(P1a 踩过一次)。比较前一律用
|
||||
`_as_utc()`(`services/yyb_service.py` 与 `services/automation/scheduler.py` 各有一份)补时区。
|
||||
|
||||
@@ -130,6 +130,22 @@ class TestCkHealthSelection(_Base):
|
||||
assert ids[0] == never.id
|
||||
assert stale.id in ids
|
||||
|
||||
def test_generated_sql_is_mysql_compatible(self):
|
||||
"""回归:选行 SQL 不能出现 MySQL 不认识的 NULLS FIRST。
|
||||
|
||||
生产是 MySQL、本地测试是 SQLite(SQLite 支持 NULLS FIRST,所以行为测试
|
||||
发现不了),这里按 MySQL 方言编译一次断言。
|
||||
"""
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
query = ck_health.build_select_query(
|
||||
self.session, self._job(), datetime.now(UTC)
|
||||
)
|
||||
sql = str(query.statement.compile(dialect=mysql.dialect())).upper()
|
||||
assert "NULLS FIRST" not in sql and "NULLS LAST" not in sql
|
||||
# 「没检测过的优先」要用可移植写法表达
|
||||
assert "IS NULL DESC" in sql
|
||||
|
||||
def test_batch_size_caps_the_round(self):
|
||||
job = self._job()
|
||||
job.batch_size = 2
|
||||
|
||||
@@ -24,11 +24,15 @@ JOB_KEY = "ck_health"
|
||||
FLUSH_EVERY = 25
|
||||
|
||||
|
||||
def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginTask]:
|
||||
"""挑本轮要检测的记录。
|
||||
def build_select_query(db: Session, job: AutomationJob, now: datetime):
|
||||
"""构造本轮要检测记录的查询(与取行分开,便于按方言编译做回归)。
|
||||
|
||||
口径与人工点检测一致(``cookie_operable_filter``:持有 CK 且没有在途尝试),
|
||||
再加三条:距上次检测超过作业间隔、账号允许自动管理、最久没查的优先。
|
||||
|
||||
排序刻意不用 ``NULLS FIRST``:那是 PostgreSQL/SQLite 语法,**MySQL 会报
|
||||
1064 语法错误**(生产踩过一次)。改用 ``ck_checked_at IS NULL DESC``,
|
||||
三个方言下语义一致且都是合法 SQL。
|
||||
"""
|
||||
cutoff = now - timedelta(seconds=max(int(job.interval_seconds or 0), 60))
|
||||
return (
|
||||
@@ -42,12 +46,19 @@ def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginT
|
||||
LoginTask.ck_checked_at < cutoff,
|
||||
)
|
||||
)
|
||||
.order_by(LoginTask.ck_checked_at.asc().nullsfirst(), LoginTask.id.asc())
|
||||
.order_by(
|
||||
LoginTask.ck_checked_at.is_(None).desc(),
|
||||
LoginTask.ck_checked_at.asc(),
|
||||
LoginTask.id.asc(),
|
||||
)
|
||||
.limit(max(int(job.batch_size or 0), 1))
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginTask]:
|
||||
return build_select_query(db, job, now).all()
|
||||
|
||||
|
||||
def _detect(task: LoginTask) -> dict[str, Any]:
|
||||
try:
|
||||
return check_douyu_cookie(task.cookie or "")
|
||||
|
||||
Reference in New Issue
Block a user