fix(db): run migrations on Chinese, Japanese and Korean Windows

alembic reads alembic.ini with encoding="locale". On Python 3.11 that is
the Windows ANSI code page even under PYTHONUTF8=1, which the desktop
shell sets, so the em dashes in the ini's comments raised
UnicodeDecodeError inside Config() on cp932/cp936/cp949/cp950 systems.
_run_alembic_upgrade treats that as "alembic unavailable": every startup
logged "alembic upgrade head skipped: 'cp949' codec can't decode byte
0xe2", never took the pre-migration backup, and never ran a migration --
including the data-healing ones (0006/0007) that the additive column
reconcile cannot replace.

The ini is now ASCII, with a note saying why, and a test parses it in
each of those code pages with the parser alembic builds and pins it
ASCII. Same Python 3.11 locale-decoding class as the .pth fix in #1795.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kevin9327
2026-09-14 08:40:02 +09:00
co-authored by Claude Opus 5
parent eaf8bb9538
commit 61f99debb4
2 changed files with 49 additions and 6 deletions
+10 -6
View File
@@ -1,25 +1,29 @@
# Alembic configuration for VoiceStudio.
# Run from anywhere: alembic -c <repo>/alembic.ini <command>
# Default commands:
# alembic upgrade head apply all pending migrations
# alembic revision -m "" create a new migration
# alembic current show current schema version
# alembic upgrade head - apply all pending migrations
# alembic revision -m "..." - create a new migration
# alembic current - show current schema version
#
# Keep this file ASCII: alembic reads it in the locale code page, which on a
# Chinese, Japanese or Korean Windows cannot decode UTF-8 punctuation
# (tests/test_alembic_ini_locale.py).
#
# DB URL is resolved dynamically from core.config (env aware).
# See backend/migrations/env.py.
[alembic]
# %(here)s = this file's directory. Alembic resolves bare relative paths
# against the process CWD, not the ini and the app doesn't always start
# against the process CWD, not the ini - and the app doesn't always start
# from the repo root (`tauri dev` runs the backend with
# cwd=frontend/src-tauri), which made startup migrations die with
# "Path doesn't exist: backend/migrations" the first time one was pending.
script_location = %(here)s/backend/migrations
prepend_sys_path = %(here)s/backend
# Split multi-path options on os.pathsep, not the legacy space/comma/colon
# set a colon-split would shred "C:\..." absolute paths on Windows.
# set - a colon-split would shred "C:\..." absolute paths on Windows.
path_separator = os
# sqlalchemy.url is set programmatically in env.py do NOT set it here.
# sqlalchemy.url is set programmatically in env.py - do NOT set it here.
sqlalchemy.url =
[loggers]
+39
View File
@@ -0,0 +1,39 @@
"""alembic.ini must parse in every Windows ANSI code page.
alembic reads its ini with ``encoding="locale"`` (``alembic.util.compat.
read_config_parser``). On Python 3.11 that is the Windows ANSI code page even
with ``PYTHONUTF8=1``, which the desktop shell sets — so on a Chinese, Japanese
or Korean Windows the em dashes in the ini's comments raised
``UnicodeDecodeError`` inside ``Config()``, and every startup logged
"alembic upgrade head skipped" and never ran a migration or took the
pre-migration backup.
"""
from __future__ import annotations
import configparser
from pathlib import Path
import pytest
_INI = Path(__file__).resolve().parents[1] / "alembic.ini"
@pytest.mark.parametrize("code_page", ["cp932", "cp936", "cp949", "cp950", "cp1252"])
def test_alembic_ini_parses_in_the_windows_ansi_code_page(code_page):
# The same parser alembic.config.Config builds: `here` is its default.
parser = configparser.ConfigParser({"here": str(_INI.parent)})
assert parser.read(_INI, encoding=code_page) == [str(_INI)]
assert parser.get("alembic", "script_location").endswith("/backend/migrations")
def test_alembic_ini_is_ascii_so_no_locale_can_break_it():
data = _INI.read_bytes()
offenders = [
f"line {n}: {line.decode('utf-8', 'replace').strip()}"
for n, line in enumerate(data.splitlines(), start=1)
if any(b > 0x7F for b in line)
]
assert not offenders, (
"alembic.ini is read in the locale code page, so it must stay ASCII:\n "
+ "\n ".join(offenders)
)