Копии и выкат

Первое подключение проходило, повторное — «не удалось сохранить»

upsert не берёт частичный уникальный индекс: Postgres отвечает 42P10 на планировании. Ломается только второй раз, и в интерфейсе это выглядит случайным сбоем.

Александр Цапков10 сентября 20262 мин

Уникальность «один сервис на компанию» у нас стояла частичным индексом — с условием, потому что вебхуков на компанию бывает много, а всего остального по одному:

CREATE UNIQUE INDEX ... ON integrations (organization_id, kind)
  WHERE kind <> 'webhook';

Клиент сохранял настройку через upsert с onConflict: "organization_id,kind". И получал отказ — но не всегда.

Что именно ломалось

Postgres не выводит частичный индекс из голого списка колонок. Чтобы он его взял, тот же предикат нужен в самом запросе, а через PostgREST его не передать. Ответ приходит такой:

42P10 there is no unique or exclusion constraint matching
the ON CONFLICT specification

Коварство в том, что первое подключение проходило. Конфликта не было, запись вставлялась — но запрос всё равно шёл с ON CONFLICT, и падал он на планировании, а не на записи. То есть отказ зависел не от данных, а от того, дошёл ли планировщик до разбора этой конструкции.

Ломалось только повторное сохранение. В интерфейсе это выглядело как случайный сбой: «первый раз сохранилось, второй — нет», и воспроизвести на пустой базе не получалось.

Чем лечится

Явным путём вместо upsert: найти → обновить или вставить, с обработкой 23505 как гонки. Дороже на один запрос и полностью предсказуемо.

Соблазн «сделать индекс полным, чтобы upsert заработал» мы отвергли: условие в индексе стоит там не для красоты, а потому что вебхуков на компанию действительно много. Подгонять схему под удобство клиента — менять правду о данных ради синтаксиса.

Приём: проверить, ничего не записав в боевую базу

Отдельно стоит запомнить, потому что применимо шире этого случая.

Отправьте тот же запрос с заведомо несуществующим внешним ключом.

  • 42P10 возникает на планировании и приходит раньше проверки ключа. Если вы получили его — запрос сломан, и до записи дело не дошло.
  • 23503 (нарушение внешнего ключа) означает, что запрос валиден: план построился, и остановила его только проверка ключа. Ничего не создалось.

Так проверяется форма запроса на живой базе, без тестовой записи и без уборки за собой.

Что из этого следует

Отказ, который зависит от порядка обработки запроса, а не от данных, всегда выглядит случайным — и потому ищется дольше всего. Здесь ошибка была в первом же подключении, но проявлялась только со второго, и вся диагностика уходила в данные, где её не было.

Признак такого класса ошибок один: не воспроизводится на чистой базе, но устойчиво воспроизводится на второй попытке. Если видите это — смотрите не в данные, а в план запроса.


У нас уведомления уходят своим ботом и своим релеем, а настройки каналов хранятся на компанию — тот самый случай, где эта уникальность и понадобилась. Как это устроено — Уведомления в Telegram.

Уведомления о задачах в Telegram своим ботомУведомления о задачах приходят в Telegram ботом вашей компании, а не общим ботом платформы.

Читайте также

Автор

Александр ЦапковОснователь Скоупворк

Веду платформу и её боевой контур сам: разработка, выкат, дежурство. Пишу о том, на чём мы обожглись, — с датами, замерами и ссылками на решения в репозитории.