Skip to main content

Migrating the StormBPMN Database to UTF-8

About 5 min

Migrating the StormBPMN Database to UTF-8

A guide for the database administrator of an on-premise installation. The goal is to convert the StormBPMN database from SQL_ASCII to UTF-8 to ensure that case-insensitive search and deduplication for Cyrillic characters work correctly.

All commands and thresholds have been verified on PostgreSQL 16, database SQL_ASCII | C | C (matching the target installation). Before proceeding, read the entire document, especially section 3 (data diagnostics).


1. Why

StormBPMN supports databases only in UTF-8 encoding β€” this is an official requirement of Storm Storm Enterprise (installopen in new window, get-startedopen in new window): "UTF8 encoding β€” correct text storage, ICU collation, and natural sorting of names". This instruction aligns the installation on SQL_ASCII with this requirement.

In addition to the official requirement: encoding alone is insufficient β€” a UTF-8-compatible locale is also required. ICU collation fixes sorting, but case handling (lower(), case-insensitive search) depends on LC_CTYPE. On SQL_ASCII (and on UTF-8 with the C locale), the lower() function does not convert Cyrillic characters to lowercase β†’ case-insensitive search for Russian names/titles (employees, roles, diagrams, departments, comments, etc.) and deduplication during import do not work correctly: searching for "ΠΈΠ²Π°Π½ΠΎΠ²" does not find "Иванов".

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 βœ…

Important: changing only encoding is not enough β€” the locale (LC_CTYPE) must also be UTF-8-compatible (C.UTF-8, ru_RU.UTF-8, en_US.UTF-8), not C.


2. Preparation (pre-flight)


3. Diagnostics (MANDATORY before migration)

3.1. Current encoding/locale

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

-- ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒΠ½Π°Ρ ΠΏΡ€ΠΎΠ±Π°: Ссли Π²Π΅Ρ€Π½ΡƒΠ»ΠΎΡΡŒ 'Иванов' (рСгистр Π½Π΅ измСнился) β€” миграция Π½ΡƒΠΆΠ½Π°.
SELECT lower('Иванов');

3.2. Are the data valid UTF-8 β€” a critical step

SQL_ASCII stores bytes "as is," without validation. The subsequent fate of the migration depends on the encoding in which the Cyrillic characters are actually stored:

  • Storm writes data as UTF-8 (the driver uses client_encoding=UTF8) β€” the expected case, in which the migration is straightforward (scenario A).
  • However, if data from legacy sources or manual edits in CP1251 (Windows-1251) or another single-byte encoding ended up in the database, it cannot simply be "renamed" to UTF-8; transcoding is required (scenario B).

Quick indicator (single-byte vs. UTF-8). For UTF-8, Cyrillic characters occupy 2 bytes per character, while for CP1251 they occupy 1 byte. Take a known value and compare its length in bytes:

-- для 'Иванов' (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;

Reliable check β€” "test dump to UTF-8". pg_dump --encoding=UTF8 fails on the very first invalid byte, thus serving as a validator for the entire database at once:

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.

List of specific corrupted strings (if targeted cleanup is needed) β€” for each text column, for example:

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 $$;

⚠️ Dangerous nuance. A standard pg_dump (without --encoding=UTF8) + pg_restore will silently carry over corrupted bytes to the new UTF-8 database (restore inherits client_encoding=SQL_ASCII from the dump and does not validate). The database will be "UTF-8," but it will contain invalid byte mines inside. Therefore, always use --encoding=UTF8 for dumps β€” it prevents dirty data from migrating unnoticed.


4. Migration (with downtime)

The encoding of the existing database cannot be changed in place (ALTER DATABASE ... ENCODING does not exist) β€” only dump β†’ new UTF-8 database β†’ 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.

Scenario B β€” data in CP1251 (if step 2 failed)

Transcoding is required. It is safer to do this on a text dump via iconv (ASCII parts are invariant):

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

If the database mixes UTF-8 and CP1251 (a common result of a long life with SQL_ASCII) β€” a single iconv is not enough; such cases are handled by the DBA line by line (based on the list from 3.2). Do not migrate "blindly".


5. Post-migration verification

\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. Rollback

  • Do not delete the old database until the new one has been verified and the application has run normally.
  • Rollback = revert the application's datasource to the old database and restart the backend.

7. Notes

  • --template=template0 is required when changing the locale relative to template1.
  • After switching to UTF-8, ICU-collation natural_sort will be applied (changeset DEV-871 intentionally skipped it for non-UTF8) β€” "human" sorting of numbers in names.
  • Extensions/roles/sequences pg_dump -Fc/pg_restore are migrated automatically, but the receiver binary must contain the packages (hence the image pgvector/pgvector:pg17); pg_trgm Storm requires explicit creation (step 4, item 5). In case of owner mismatch β€” --no-owner --role=<user>, create roles in advance.

8. If migration is not performed

Case-insensitive search and deduplication for Cyrillic characters on SQL_ASCII will not work β€” this is a limitation of the specific installation, not a defect of Stormbpmn. Workarounds on the application side for unsupported encodings are not provided.