import type pg from "pg"; import type { TableInfo } from "../types.js"; /** * The most rules we will lift for one table. * * Each rejected insert names at most one rule, so a table guarded by several * needs several passes. A table that is still refusing after this many is * telling us something we do not understand, and the right answer to that is to * stop and report it, not to keep pulling the schema apart. */ export declare const MAX_WIDENINGS = 6; /** * Refuse to issue anything that is not one of the four permitted statements. * * `checkDef` is the definition {@link restoreStatementFor} already validated; * it is elided from the skeleton so that a legitimate CHECK expression * mentioning, say, a column called `grant_kind` cannot be mistaken for an * attempt to touch a grant, and so that no CHECK body can widen what the * shapes accept. */ export declare function assertSuspensionSafe(sql: string, checkDef?: string): void; /** * The statement that would put this CHECK constraint back, or null if we cannot * be certain of one. * * Validated *before* the constraint is dropped, never after: dropping something * we then discover we cannot restore would leave us probing a schema we had * weakened, and there is no recovery from that except failing the run. The * definition comes from `pg_get_constraintdef`, so it is Postgres' own * rendering of a parsed expression rather than anything a user typed — but it * is still the one piece of text here that is not a fixed keyword, so it is * checked for shape and for balanced quoting rather than trusted. * * `NOT VALID` is required. The row we are about to plant is precisely the row * the constraint refuses, so a validating `ADD CONSTRAINT` would fail; `NOT * VALID` skips the existing rows and still enforces the rule on everything the * probes go on to write. */ export declare function restoreStatementFor(table: TableInfo, name: string, def: string): string | null; /** * The visibility column a CHECK constrains, if it constrains one. * * The third structural invariant says a planted row is seeded to its most * private state, so that a cross-user read of it can never be explained away as * "that row was published on purpose". For an enumerated column, the CHECK * *is* the statement of what private looks like — `visibility in ('private', * 'team', 'public')` is the only thing in the catalog that says `private` is * available. Lift its enforcement and the fallback generator writes a value * outside the vocabulary entirely, and the row lands in a visibility state the * application has no meaning for: not private, not public, and useless as * evidence either way. A policy reading `visibility <> 'private'` would then * let a stranger read it exactly as designed, and we would call that a leak. * * So this one class of constraint is never lifted, and the table is honestly * skipped instead. Matched on the constraint's own text rather than on its * name, because names are the developer's to choose and the expression is not. */ export declare function visibilityColumnIn(table: TableInfo, def: string): string | null; /** * The visibility column whose private state we could not name, if there is one. * * The other half of the third invariant, and the sharper half. A table may * enumerate no private state at all — `visibility in ('public', 'link_only')` * is a real shape, and every row in such a table is one the application shows * to everybody. Synthesis will happily pick `public` out of that list and the * insert succeeds, so no constraint is ever lifted and Guard A above never * fires; but the row we planted is published *by design*, a stranger reading it * is behaving exactly as the schema intends, and reporting that as a leak is a * false positive we manufactured ourselves. * * Cloning already refuses this case — see `looksPublic` — and suspension is the * more invasive strategy, so it refuses it too. The table is named as unchecked * instead, which is the fourth invariant: silence beats a guess. * * `status` counts only when it actually enumerates a visibility state, exactly * as ownership inference qualifies it. An open ticket is not a published one, * and refusing every table with a `status` column would cost most of a schema * for nothing. */ export declare function unprovablyPrivateColumn(table: TableInfo): string | null; /** What Postgres said about an insert it rejected, in the terms suspension needs. */ export interface InsertFailure { code?: string | undefined; constraint?: string | undefined; } /** The outcome of trying to lift one more rule out of the way. */ export type Widening = { widened: true; note: string; } /** `reason` is null when there was simply nothing left to try. */ | { widened: false; reason: string | null; }; /** * The rules lifted for one table, and the obligation to put them back. * * One of these lives for the length of a single table's seeding. Nothing * outside `src/seed` should hold one across a phase boundary — the point is * that a probe never sees a suspended schema. */ export declare class Suspension { private readonly client; private readonly table; private readonly checks; private readonly triggers; private triggersTried; /** Descriptions of what was lifted, for the run's output. */ readonly notes: string[]; constructor(client: pg.PoolClient, table: TableInfo); /** Whether anything is currently lifted. */ get active(): boolean; /** * Lift one more rule, chosen from what Postgres said about the last refusal. * * A rejected insert names at most one rule, so this is called in a loop: drop * the CHECK the error named, try again, and if the next refusal comes from a * trigger instead, disable the triggers. Only a refusal the *database* issued * is ever acted on — a row we could not even build, because a required * foreign key had no parent to point at, is not a rule to be lifted and the * caller does not offer it here. */ widen(failure: InsertFailure): Promise; /** * Drop one CHECK constraint, having first established that it is a CHECK * constraint on this table and that we know how to put it back. * * The `contype = 'c'` filter is read from the live catalog rather than from * the snapshot, and it is the structural reason a foreign key or a unique * constraint can never be dropped by a name collision. */ private dropCheck; /** * Disable the table's own triggers. * * Named one at a time rather than with `DISABLE TRIGGER USER`, so that a * trigger the developer had already disabled is not silently switched on * again by the matching `ENABLE`. Only `tgisinternal = false` triggers are * ever named, which is what keeps foreign key enforcement — implemented as * internal constraint triggers — completely untouched. Only triggers enabled * in the ordinary way (`tgenabled = 'O'`) are taken; an `ENABLE ALWAYS` * trigger is left alone rather than quietly downgraded. */ private disableTriggers; /** * Put every lifted rule back. * * Called before any probe runs, and it must succeed. If it cannot, the run is * stopped: continuing would mean asking a weakened schema whether it is * secure, and answering that question wrongly is the one thing this tool * cannot do. Nothing has been committed either way — the outer transaction is * rolled back regardless — so the failure costs a run, not a database. */ restore(): Promise; private triggerSql; /** * Issue one statement, with a lock timeout, and translate the two failures a * developer needs told apart. * * The statement is confined to a savepoint so that a refusal costs this one * table rather than poisoning the transaction the rest of the seeding runs * in. */ private execute; /** * The session's current `lock_timeout`, in a form safe to write back. * * Postgres renders it as `0`, `100ms`, `5s` and so on. Anything that is not * one of those shapes is replaced with `0` — the default — rather than * interpolated, so this can never become a way to put arbitrary text into a * statement. */ private currentLockTimeout; }