Перейти к основному содержанию

Миграция БД StormBPMN на UTF-8

Около 5 мин

Миграция БД StormBPMN на UTF-8

Руководство для администратора БД on-premise инсталляции. Цель — перевести базу StormBPMN с SQL_ASCII на UTF-8, чтобы корректно работал регистронезависимый поиск и дедупликация по кириллице.

Все команды и пороги проверены на PostgreSQL 16, БД SQL_ASCII | C | C (как у целевой инсталляции). Перед выполнением — прочитать целиком, особенно раздел 3 (диагностика данных).


1. Зачем

StormBPMN поддерживает БД только в кодировке UTF-8 — это официальное требование Storm Enterprise (installopen in new window, get-startedopen in new window): «Кодировка UTF8 — корректное хранение текста, ICU-коллация и натуральная сортировка названий». Эта инструкция приводит инсталляцию на SQL_ASCII в соответствие с ним.

Дополнительно к официальному требованию: одной только кодировки мало — нужна и UTF-8-совместимая локаль. ICU-коллация чинит сортировку, но регистр (lower(), регистронезависимый поиск) зависит от LC_CTYPE. На SQL_ASCII (и на UTF-8 с локалью C) функция lower() не приводит кириллицу к нижнему регистру → регистронезависимый поиск по русским именам/названиям (сотрудники, роли, диаграммы, подразделения, комментарии и т.д.) и дедуп при импорте работают неверно: поиск «иванов» не находит «Иванов».

encoding | LC_CTYPElower('Иванов')'Иванов' ILIKE '%иванов%'
SQL_ASCII | CИвановfalse
UTF8 | CИвановfalse
UTF8 | C.UTF-8ивановtrue
UTF8 | ru_RU.UTF-8 / en_US.UTF-8ивановtrue

Важно: сменить только encoding недостаточно — локаль (LC_CTYPE) тоже должна быть UTF-8-совместимой (C.UTF-8, ru_RU.UTF-8, en_US.UTF-8), а не C.


2. Подготовка (pre-flight)


3. Диагностика (ОБЯЗАТЕЛЬНО до миграции)

3.1. Текущая кодировка/локаль

SELECT datname,
       pg_encoding_to_char(encoding) AS encoding,
       datcollate, datctype
FROM pg_database
WHERE datname = current_database();

-- Контрольная проба: если вернулось 'Иванов' (регистр не изменился) — миграция нужна.
SELECT lower('Иванов');

3.2. Валидны ли данные как UTF-8 — критический шаг

SQL_ASCII хранит байты «как есть», без проверки. Дальнейшая судьба миграции зависит от того, в какой кодировке реально лежит кириллица:

  • Storm пишет данные как UTF-8 (драйвер использует client_encoding=UTF8) — ожидаемый случай, тогда миграция простая (сценарий A).
  • Но если в БД попадали данные из легаси-источников/ручных правок в CP1251 (Windows-1251) или иной однобайтовой кодировке — их нельзя просто «переобозвать» в UTF-8, нужно транскодирование (сценарий B).

Быстрый признак (однобайтовая vs UTF-8). Для UTF-8 кириллица занимает 2 байта/символ, для CP1251 — 1 байт. Возьмите заведомо известное значение и сравните длину в байтах:

-- для 'Иванов' (6 символов): 12 → UTF-8; 6 → однобайтовая (CP1251)
SELECT octet_length(last_name) AS bytes, last_name
FROM sm_persons
WHERE last_name IS NOT NULL
ORDER BY id LIMIT 20;

Надёжная проверка — «пробный дамп в UTF-8». pg_dump --encoding=UTF8 падает на ПЕРВОМ же невалидном байте, то есть служит валидатором всей БД сразу:

pg_dump -U <user> --encoding=UTF8 -Fc -d <db> -f /tmp/storm.utf8.dump
# Успех (exit 0) → данные — валидный UTF-8, идём по сценарию A (этот же дамп и используем).
# Ошибка вида:
#   pg_dump: detail: invalid byte sequence for encoding "UTF8": 0xc8 0xe2
# → есть «битые»/однобайтовые данные, сценарий B.

Список конкретных битых строк (если нужно точечно почистить) — по каждой текстовой колонке, например:

DO $$
DECLARE r record; bad int[] := '{}';
BEGIN
  FOR r IN SELECT id, last_name AS v FROM sm_persons LOOP
    BEGIN PERFORM convert_from(r.v::bytea, 'UTF8');
    EXCEPTION WHEN others THEN bad := bad || r.id; END;
  END LOOP;
  RAISE NOTICE 'sm_persons с битыми байтами (id): %', bad;
END $$;

⚠️ Опасный нюанс. Обычный pg_dump (без --encoding=UTF8) + pg_restore молча протащит битые байты в новую UTF-8 БД (restore наследует client_encoding=SQL_ASCII из дампа и не валидирует). БД будет «UTF-8», но с невалидными байтами-минами внутри. Поэтому для дампа всегда используйте --encoding=UTF8 — он не даст мигрировать грязные данные незаметно.


4. Миграция (с даунтаймом)

Кодировку существующей БД нельзя сменить на месте (ALTER DATABASE ... ENCODING не существует) — только dump → новая UTF-8 БД → restore.

# 0. Бэкап уже сделан (раздел 2).

# 1. Остановить приложение (Storm backend), чтобы не было записи.

# 2. Дамп + валидация одним шагом (см. 3.2). Если упал — сценарий B, см. ниже.
pg_dump -U <user> --encoding=UTF8 -Fc -d <db> -f /tmp/storm.utf8.dump

# 3. Создать новую UTF-8 БД с UTF-8-локалью (обязательно из template0).
createdb -U <user> --encoding=UTF8 \
         --lc-ctype=ru_RU.UTF-8 --lc-collate=ru_RU.UTF-8 \
         --template=template0 storm_utf8
#   (нет ru_RU.UTF-8 → используйте C.UTF-8)
#   Альтернатива — новый кластер: initdb --encoding=UTF8 --locale=ru_RU.UTF-8 -D <newdir>

# 4. Restore. PGCLIENTENCODING=UTF8 гарантирует валидацию на вставке.
PGCLIENTENCODING=UTF8 pg_restore -U <user> -d storm_utf8 -j 4 /tmp/storm.utf8.dump
#   -j 4 — параллельные воркеры (ускоряет; подберите по числу ядер).
#   При проблемах с владельцами/ролями: добавьте --no-owner --role=<user>.

# 5. Обязательные расширения. pg_dump переносит их сам, но проверяем явно
#    (Storm требует создавать pg_trgm вручную, иначе автокомплит молча не работает):
psql -U <user> -d storm_utf8 <<'EOSQL'
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS hstore;
EOSQL

# 6. Переключить datasource приложения на storm_utf8 и запустить backend.

Сценарий B — данные в CP1251 (если шаг 2 упал)

Нужно транскодировать. Безопаснее — на текстовом дампе через iconv (ASCII-части инвариантны):

pg_dump -U <user> -Fp -d <db> -f /tmp/storm.plain.sql          # plain-text дамп (как есть)
iconv -f WINDOWS-1251 -t UTF-8 /tmp/storm.plain.sql -o /tmp/storm.win1251-to-utf8.sql
# Убедиться, что в шапке дампа client_encoding = UTF8 (поправить при необходимости),
# затем восстановить в UTF-8 БД из раздела 4 шаг 3:
PGCLIENTENCODING=UTF8 psql -U <user> -d storm_utf8 -f /tmp/storm.win1251-to-utf8.sql

Если в БД смешаны UTF-8 и CP1251 (частый итог долгой жизни SQL_ASCII) — единого iconv мало; такие случаи разбираются DBA построчно (по списку из 3.2). Не мигрируйте «вслепую».


5. Проверка после миграции

\c storm_utf8
-- кодировка/локаль
SELECT pg_encoding_to_char(encoding), datcollate, datctype
FROM pg_database WHERE datname = current_database();   -- ожидаем UTF8 + *.UTF-8

-- фолдинг кириллицы
SELECT lower('Иванов');                                -- ожидаем 'иванов'

-- данные на месте и не «кракозябры»
SELECT count(*) FROM sm_persons;                       -- сверить с источником
SELECT id, last_name, first_name FROM sm_persons ORDER BY id LIMIT 10;

-- обязательные расширения на месте (ожидаем 5 строк)
SELECT extname FROM pg_extension
WHERE extname IN ('vector','pg_trgm','pgcrypto','uuid-ossp','hstore') ORDER BY extname;

6. Откат

  • Старую БД не удалять, пока новая не проверена и приложение не отработало штатно.
  • Откат = вернуть datasource приложения на старую БД и перезапустить backend.

7. Замечания

  • --template=template0 обязателен при смене локали относительно template1.
  • После перехода на UTF-8 начнёт применяться ICU-collation natural_sort (changeset DEV-871 намеренно пропускал её на не-UTF8) — «человеческая» сортировка чисел в именах.
  • Расширения/роли/последовательности pg_dump -Fc/pg_restore переносят сами, но бинарь приёмника должен содержать пакеты (отсюда образ pgvector/pgvector:pg17); pg_trgm Storm требует создавать явно (шаг 4, п. 5). При расхождении владельцев — --no-owner --role=<user>, роли создать заранее.

8. Если миграцию не делают

Регистронезависимый поиск и дедуп по кириллице на SQL_ASCII работать не будут — это ограничение конкретной инсталляции, а не дефект StormBPMN. Обходные решения на стороне приложения для неподдерживаемой кодировки не предоставляются.