From 61f99debb444c37ba3dc92a015434ad1a5f342e8 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:40:02 +0900 Subject: [PATCH] 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 --- alembic.ini | 16 ++++++++----- tests/test_alembic_ini_locale.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 tests/test_alembic_ini_locale.py diff --git a/alembic.ini b/alembic.ini index c2d57a8e..123dc4ab 100644 --- a/alembic.ini +++ b/alembic.ini @@ -1,25 +1,29 @@ # Alembic configuration for VoiceStudio. # Run from anywhere: alembic -c /alembic.ini # 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] diff --git a/tests/test_alembic_ini_locale.py b/tests/test_alembic_ini_locale.py new file mode 100644 index 00000000..d2bd86fa --- /dev/null +++ b/tests/test_alembic_ini_locale.py @@ -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) + )