Raw SQL používá sloupce, které chybí v backfillu tenantového schématu
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”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.
Root cause
Sekce “Root cause”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 zapnoutChybu maskovaly tři nezávislé vrstvy:
- Guard psaný jako záměrně padající dotaz. Detekce přítomnosti
sloupce přes
SELECT ext_refresh_token FROM … LIMIT 0vtry/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. - Vnější
catch, který chyby „does not exist” nezapisuje. Drift se tak nikdy neobjeví ve výsledku úlohy. - Guard ověřující jeden sloupec z jedenácti. Částečný drift projde a spadne až následující dotaz.
Fix
Sekce “Fix”// 1) Sdílený kontrakt — jediný zdroj pravdy pro ensure, cron i testyexport 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 APIexport 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 dotry/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 oSELECT. 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 TYPEv 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.