monitor_run.trigger 是 MySQL 保留字,渲染出来是 `trigger`。这比之前手写的列清单更正确: 清单里的裸 trigger 会直接语法错误。断言改为剥掉引号后比对列名。
84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Guards for the in-place column migration.
|
|
|
|
``create_all`` creates missing tables but never adds columns to a table that
|
|
already exists, so new model columns are applied by ``_ensure_columns``. That step
|
|
was originally driven by a hand-kept list, and forgetting to update it did not
|
|
fail loudly -- the app still started, connected, and then failed on every query
|
|
and every scheduler tick. These tests pin down its replacement, which derives the
|
|
work from the model metadata.
|
|
"""
|
|
|
|
import pytest
|
|
from sqlalchemy import Boolean, Column, Integer, MetaData, String, Table
|
|
|
|
from api.monitor import db
|
|
from api.monitor.models import MonitorBase
|
|
|
|
|
|
def _migratable_columns():
|
|
for table in MonitorBase.metadata.sorted_tables:
|
|
for column in table.columns:
|
|
if column.primary_key:
|
|
continue
|
|
yield pytest.param(column, id=f"{table.name}.{column.name}")
|
|
|
|
|
|
def _ddl(column: Column) -> str:
|
|
"""Render a detached column, so the tests never mutate the real metadata."""
|
|
scratch = Table("scratch", MetaData(), column)
|
|
return db._column_ddl(scratch.columns[0])
|
|
|
|
|
|
@pytest.mark.parametrize("column", _migratable_columns())
|
|
def test_column_renders_as_ddl(column):
|
|
ddl = db._column_ddl(column)
|
|
|
|
# SQLAlchemy back-quotes an identifier only when it has to, so the rendered
|
|
# name matches the column's with the quoting stripped -- which is the case for
|
|
# reserved words like monitor_run.trigger. That quoting is a feature: the
|
|
# hand-kept list this replaced would have emitted bare `trigger` and died on a
|
|
# syntax error.
|
|
assert ddl.split(" ", 1)[0].strip("`") == column.name
|
|
# MySQL refuses AUTO_INCREMENT together with the DEFAULT this helper appends
|
|
# to NOT NULL columns.
|
|
assert "AUTO_INCREMENT" not in ddl
|
|
|
|
|
|
@pytest.mark.parametrize("column", _migratable_columns())
|
|
def test_not_null_columns_carry_a_default(column):
|
|
"""So ADD COLUMN cannot fail on a table that already holds rows.
|
|
|
|
Without a DEFAULT, whether the ALTER succeeds depends on the server's
|
|
sql_mode -- not something a deployment should hinge on.
|
|
"""
|
|
if column.nullable:
|
|
pytest.skip("nullable column needs no seed value")
|
|
|
|
assert "DEFAULT" in db._column_ddl(column)
|
|
|
|
|
|
def test_boolean_default_becomes_a_mysql_literal():
|
|
"""Python's True is not a SQL keyword; it has to become 1."""
|
|
assert "DEFAULT 1" in _ddl(Column("flag", Boolean, nullable=False, default=True))
|
|
|
|
|
|
def test_string_defaults_are_quoted():
|
|
assert "DEFAULT 'interval'" in _ddl(
|
|
Column("mode", String(16), nullable=False, default="interval")
|
|
)
|
|
|
|
|
|
def test_integer_defaults_are_not_quoted():
|
|
ddl = _ddl(Column("n", Integer, nullable=False, default=0))
|
|
|
|
assert "DEFAULT 0" in ddl
|
|
assert "DEFAULT '0'" not in ddl
|
|
|
|
|
|
def test_a_not_null_column_without_a_model_default_falls_back_to_zero():
|
|
"""Belt and braces: even a column the model gives no default still migrates."""
|
|
ddl = _ddl(Column("n", Integer, nullable=False))
|
|
|
|
assert "DEFAULT 0" in ddl
|