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.
| Mode | Inner list | Registry | Output |
|---|---|---|---|
| Persist | Persistent CREATE / ALTER / DROP FEATURES / permission DDL only | Final DAG committed, or nothing | Mutation report only (CREATED / REPLACED / ALTERED / DELETED / SKIPPED) |
| Local | CREATE [OR REPLACE] TEMPORARY, ALTER TEMPORARY, DROP TEMPORARY, then one result | Unchanged | That trailing RETURN / SHOW / DESCRIBE / EXPLAIN |
| Wrapped query | No mutations, one result | Unchanged | That 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.