Skip to content

feat(db): перевести хранилище на PGlite ради нативных таймстемпов - #21

Merged
mokevnin merged 1 commit into
mainfrom
feat/pglite-native-timestamps
Aug 24, 2026
Merged

feat(db): перевести хранилище на PGlite ради нативных таймстемпов#21
mokevnin merged 1 commit into
mainfrom
feat/pglite-native-timestamps

Conversation

@mokevnin

Copy link
Copy Markdown
Contributor

Таймстемпы лежали в integer с unix-миллисекундами, и что в колонке время, знал только кодек drizzle да комментарий рядом. Под drizzle теперь postgres — PGlite, wasm-сборка внутри процесса, — и колонки объявлены timestamptz.

Отдельного сервиса по-прежнему нет, база так же пересоздаётся при каждом старте. Из образа ушёл apt-шаг с python3/make/g++: нативных модулей не осталось.

Шесть sqlite-миграций схлопнуты в одну — воспроизвести их на postgres нельзя, история осталась в git. id стал identity generated by default: always отвергал бы вставку с явным id, а в sqlite он был алиасом rowid и подставлялся сам.

Что вылезло при переезде

  • .run() в транзакции удаления курса — синхронный sqlite-API, теперь await на каждом delete. Там же db.get(sql\select 1`)в healthCheck сталdb.execute`.
  • 503 на всё под нагрузкой. Порог event loop delay был выставлен заведомо большим (600000), но гистограмма monitorEventLoopDelay под нагрузкой отдаёт mean = Infinity, и любое конечное значение оказывается меньше. Ноль under-pressure понимает как «проверку не делать вовсе».
  • 500 на NUL (U+0000) в тексте — нашёл контрактный прогон. Postgres такую строку не хранит в принципе, sqlite хранил. Отвечаем 400 из общего хука: это свойство хранилища, а не бизнес-правило конкретной модели, и касается каждого текстового поля. 400 задокументирован у всех операций, контракт не менялся.
  • findFirst() без ORDER BY в postgres порядок не обещает, и после UPDATE запись уезжает в конец: тесты, перечитывавшие «первую» строку, читали не ту. Перечитывания теперь по id.
  • pluginTimeout поднят с дефолтных 10 секунд: инициализация базы выросла с микросекунд до ~1.4 с (initdb плюс миграции), и запаса не хватало ни на параллельном прогоне тестов, ни на холодном старте в контейнере.

Проверено локально

pnpm test (75/75), make lint, make test-coverage (выше порогов), make migration-check, make contract-test (обе версии, несколько прогонов), make smoke-test на собранном образе, холодная pnpm install --frozen-lockfile с вынесенным node_modules.

Цена

Прогон тестов подорожал: ~2 минуты на 75 тестов, львиная доля — 70 подъёмов приложения по ~1.4 с на базу. Снимок через dumpDataDir/loadDataDir сокращает это до ~0.3 с на инстанс, но требует шва в plugins/drizzle.ts ради тестов — не делал, вынесено на обсуждение.

🤖 Generated with Claude Code

Таймстемпы лежали в integer с unix-миллисекундами, и что в колонке время,
знал только кодек drizzle да комментарий рядом. Теперь под drizzle postgres —
PGlite, wasm-сборка внутри процесса, — и колонки объявлены timestamptz: тип
говорит сам за себя. Отдельного сервиса по-прежнему нет, база так же
пересоздаётся при каждом старте, а из образа ушёл apt-шаг с python3/make/g++:
нативных модулей не осталось.

Шесть sqlite-миграций схлопнуты в одну — воспроизвести их на postgres нельзя,
история осталась в git. id стал identity generated by default: always
отвергал бы вставку с явным id, а в sqlite он был алиасом rowid и
подставлялся сам.

Что вылезло при переезде:

- .run() в транзакции удаления курса — синхронный sqlite-API, теперь await на
  каждом delete. Там же db.get(sql`select 1`) в healthCheck стал db.execute.

- 503 на всё под нагрузкой. Порог event loop delay был выставлен заведомо
  большим (600000), но гистограмма monitorEventLoopDelay под нагрузкой отдаёт
  mean = Infinity, и любое конечное значение оказывается меньше. Ноль
  under-pressure понимает как «проверку не делать вовсе».

- 500 на NUL (U+0000) в тексте — нашёл контрактный прогон. Postgres такую
  строку не хранит в принципе, sqlite хранил. Отвечаем 400 из общего хука:
  это свойство хранилища, а не бизнес-правило конкретной модели, и касается
  каждого текстового поля. 400 задокументирован у всех операций, контракт не
  менялся.

- findFirst() без ORDER BY в postgres порядок не обещает, и после UPDATE
  запись уезжает в конец: тесты, перечитывавшие «первую» строку, читали не
  ту. Перечитывания теперь по id.

- pluginTimeout поднят с дефолтных 10 секунд: инициализация базы выросла с
  микросекунд до ~1.4 с (initdb плюс миграции), и запаса не хватало ни на
  параллельном прогоне тестов, ни на холодном старте в контейнере.

Прогон тестов подорожал: ~2 минуты на 75 тестов, из них львиная доля — 70
подъёмов приложения по ~1.4 с на базу. Снимок через dumpDataDir/loadDataDir
сокращает это до ~0.3 с на инстанс, но требует шва в plugins/drizzle.ts ради
тестов и сделан не был.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mokevnin
mokevnin merged commit 71abac2 into main Aug 24, 2026
5 checks passed
@mokevnin
mokevnin deleted the feat/pglite-native-timestamps branch August 24, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant