Přeskočit na obsah

Raw SQL používá sloupce, které chybí v backfillu tenantového schématu

import { Aside } from ‘@astrojs/starlight/components’;

V logu se opakuje column "x" does not exist (PostgreSQL 42703), ale aplikace nepadá a cron hlásí errors=0. Feature se tváří dostupně, jenže u většiny tenantů ji nelze vůbec zapnout — připojení integrace končí 500. Přitom u jednoho dvou tenantů funguje bez problémů, takže se chyba dlouho odbývá jako lokální anomálie.

Druhá varianta téhož: chyba se objeví až ve chvíli, kdy někdo poprvé zpřístupní dosud nedosažitelný endpoint. Vada tam ležela měsíce.

Schéma per tenant se zakládá kódem. Během vývoje integrace si někdo přidá sloupce ručně přes psql, aby mohl testovat — a do kódu zakládajícího schéma se už nikdy nedostanou. Nový tenant je tedy nikdy nedostane.

-- kód očekává:
UPDATE "tenant_x".accounts
SET provider = 'p', ext_refresh_token = $1, ext_consent_id = $2
WHERE id = $3;
-- tenant reálně má jen:
-- id, name, number, created_at, updated_at
-- → 42703, integraci nelze zapnout

Chybu maskovaly tři nezávislé vrstvy:

  1. Guard psaný jako záměrně padající dotaz. Detekce přítomnosti sloupce přes SELECT ext_refresh_token FROM … LIMIT 0 v try/catch. Výjimka se odchytí, ale ORM ji stejně zaloguje jako chybu — při každém běhu, u každého tenanta. Vznikne šum, který maskuje reálné chyby.
  2. Vnější catch, který chyby „does not exist” nezapisuje. Drift se tak nikdy neobjeví ve výsledku úlohy.
  3. Guard ověřující jeden sloupec z jedenácti. Částečný drift projde a spadne až následující dotaz.
// 1) Sdílený kontrakt — jediný zdroj pravdy pro ensure, cron i testy
export const REQUIRED_COLUMNS = ['provider', 'ext_refresh_token', /* … */];
// 2) Detekce přes information_schema, ne přes padající dotaz.
// Ověřuje ÚPLNOU množinu, ne jeden sloupec.
export async function missingColumns(schema) {
const rows = await db.query(
`SELECT column_name FROM information_schema.columns
WHERE table_schema = $1 AND table_name = 'accounts'
AND column_name = ANY($2::text[])`,
[schema, REQUIRED_COLUMNS],
);
const present = new Set(rows.map((r) => r.column_name));
return REQUIRED_COLUMNS.filter((c) => !present.has(c));
}
// 3) Striktní repair s POSTCONDITION — a před voláním externího API
export async function ensureColumns(schema) {
if ((await missingColumns(schema)).length === 0) return;
await db.query(`ALTER TABLE "${schema}".accounts
ADD COLUMN IF NOT EXISTS provider VARCHAR(20) DEFAULT 'manual',
ADD COLUMN IF NOT EXISTS ext_refresh_token TEXT /* … */`);
const missing = await missingColumns(schema); // ← ověření, ne důvěra
if (missing.length) throw new SchemaError(missing);
}

Sloupce se zároveň doplní do všech míst, kde se schéma zakládá, a typy se převezmou z tenanta, kde integrace funguje — jinak vznikne druhý dialekt téhož schématu.

Jak se tomu vyvarovat v jiných systémech

Sekce “Jak se tomu vyvarovat v jiných systémech”
  • Detection: porovnej sloupce použité v raw SQL proti kódu zakládajícímu schéma. Vyjmenuj místa, kde se schéma vytváří — bývá jich víc, než se čeká (grep -rn "CREATE TABLE\|ADD COLUMN"). Doplň test, který kontrakt hlídá staticky; mockovaný databázový klient tuhle třídu vad nikdy nechytí, protože vrátí cokoliv.
  • Anti-pattern č. 1 — „idempotentní” repair, který spolkne chybu. Helper typu safeExec() obalí DDL do try/catch, pokračuje dál a schéma si označí za opravené. Selhání je pak nerozeznatelné od úspěchu. Repair musí mít postcondition a při neúspěchu selhat nahlas.
  • Anti-pattern č. 2 — guard jako padající dotaz. Ptej se information_schema, ne pokusem o SELECT. Odchycená výjimka je pořád výjimka a vygeneruje log.
  • Anti-pattern č. 3 — union několika zdrojů schématu v testu. Když test sloučí definice z více souborů do jedné množiny, zůstane zelený i po odstranění sloupců z jednoho z nich. Každý zdroj hodnoť samostatně.
  • Pořadí operací: dorovnání schématu musí proběhnout před voláním externího API. Jinak odejdou credentials ven do stavu, kam je pak nelze uložit.
  • Lepší přístup: jeden zdroj pravdy pro definici schématu. Je-li rozeseté do několika souborů, je to samo o sobě příčina této třídy chyb.

Sister bugs / související

Sekce “Sister bugs / související”
  • Ruční ALTER TYPE v produkci, který nedoputuje do definice schématu — stejný vzorec driftu, jiný objekt.
  • Silent truncation u agregací: chyba také nepadá, jen tiše vrací špatný výsledek.
Přidal aiarchitekt.cz · 7. 8. 2026 2:00
Provozuje aiarchitekt.cz