Transactions

START TRANSACTION and COMMIT wrap one HTTP request. They are not a DuckDB, Trino, or serving transaction.

Use the wrap for a persist script ( Create features , Drop features ) or for a request-local overlay plus one result ( Statement tools ). Mixing persist mutations with TEMPORARY mutations is UE/TRANSACTION-INVALID-SHAPE.

A script is exactly one of three shapes.

Syntax

START TRANSACTION ['message'];
    statement;
    [statement; ...]
COMMIT;

The first statement must be START TRANSACTION and the last must be COMMIT. Exactly one of each. ROLLBACK is not a statement: a failed persist transaction already rolls back.

On the managed registry, the optional string is stored as the revision MESSAGE. Bare START TRANSACTION still wraps the request; MESSAGE is then null. See Catalog revisions .

Two or more statements require this wrapper, except a single statement, unwrapped CREATE TEMPORARY plus RETURN, and a request-prefix EXPLAIN or FORMAT.

Three modes

Mode is inferred from the inner list. USE may appear only as a prefix; the last USE wins. USE qualifies CREATE names (persistent and TEMPORARY). It does not qualify RETURN locals.

ModeInner listRegistryOutput
PersistPersistent CREATE / ALTER / DROP FEATURES / permission DDL onlyFinal DAG committed, or nothingMutation report only (CREATED / REPLACED / ALTERED / DELETED / SKIPPED)
LocalCREATE [OR REPLACE] TEMPORARY, ALTER TEMPORARY, DROP TEMPORARY, then one resultUnchangedThat trailing RETURN / SHOW / DESCRIBE / EXPLAIN
Wrapped queryNo mutations, one resultUnchangedThat result
START TRANSACTION;

DROP FEATURES IF EXISTS
    FM.SALES.OLD_NET_REVENUE;

CREATE OR REPLACE FEATURES IN FM.SALES AS
PERSIST
    NET_REVENUE := GROSS_REVENUE - REFUNDS;

COMMIT;

Persist output is the net applied diff, not a per-statement log. CREATE A; DROP A of a new name writes nothing (empty report). DROP A; CREATE A of an existing name is one REPLACED. CREATE then ALTER of a new name is one CREATED. DROP … IF EXISTS that matches nothing is one SKIPPED row.

Local mutations never write the registry. Overlay replay is source order: CREATE / ALTER / DROP TEMPORARY, last write wins. ALTER TEMPORARY of a persisted name copies that node onto the request overlay and alters the copy (description, status, display format, and attributes; not FIELD or POSITION). DROP TEMPORARY of an overlay override removes only that override — a persisted feature of the same name is visible again. DROP TEMPORARY of a name that exists only in the registry hides it for this request only (no dependent protection).

Prefix EXPLAIN and FORMAT

EXPLAIN START TRANSACTION; …; COMMIT; and FORMAT START TRANSACTION; …; COMMIT; are request prefixes, not inner statements:

  • Persist inner list → dry-run or format the script; nothing is committed.
  • Local inner list → explain or format the trailing result against the overlay.
  • Wrapped query → explain or format that query.

An inner EXPLAIN is a result. Persist mode cannot include it. An inner EXPLAIN plus a second result is UE/TRANSACTION-INVALID-SHAPE, not an EXPLAIN payload.

Isolation

Persist transactions infer CREATE locally, then call lock_mutations(project_id, namespaces) before reading the registry to plan the net diff and apply. On Postgres, a known namespace set takes a shared project lock plus an exclusive lock per project_id:namespace (sorted). Unrelated namespaces proceed in parallel. A write under FM.POLICY also takes one exclusive project_id:FM.POLICY lock, so two policy writers cannot each commit a USER for the same principal. Permission DDL and unbounded WHERE / LIKE selectors take an exclusive project lock. The local store is a no-op. A failure during the store write itself rolls back; DuckDB SLT cannot prove the advisory lock.

See Registry consistency for the final-DAG check and Drop features for dependency-safe migrations.