fix: ADR 0050 callback-error precedence, and the honest limits of transaction control
CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Successful in 1m37s
CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Successful in 1m37s
Narrow contract and implementation adjustments from review. No SDK, manifest, packaging, or Signage work. Callback-error precedence: - A transaction or savepoint callback that rejects keeps its own error, even while sibling work it started is still settling (one failed half of a Promise.all). Core drains that work, rolls back, and never commits or releases. - A callback that resolves with work outstanding (a forgotten await) still fails with MODULE_DATABASE_SCOPE_WORK_OUTSTANDING and rolls back. Transaction-control SQL: - ADR 0050 forbids it through query() as a conformance rule for trusted code, not a security guarantee. Core does not parse SQL, so PostgreSQL has run the statement before Core sees it. Core detects the violation from the command tag or the transaction status, and claims nothing it cannot do: a module's own COMMIT has committed. - After a violation, the transaction's scopes refuse every later statement without sending it (it would otherwise autocommit). Core never commits the transaction itself, and the connection is destroyed rather than reused, for top-level violations too. - The commit and rollback guarantees apply to conforming use. Duplicate object-mode columns: documented as found only after execution, so a mutation can have taken effect; atomicity needs an ABI transaction. Tests prove each of these against PostgreSQL: Promise.all failures in a transaction and a savepoint keep 23505; a forgotten await still rolls back; a module COMMIT's prior work is committed while nothing after it runs; a backend PID changes after every violation; duplicate RETURNING columns persist at the top level and roll back in a transaction. ADR 0050 stays Accepted: the executor conforms to the revised text. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6db50516b7
commit
317a4ce410
@@ -105,11 +105,17 @@ export interface ModuleDatabaseTransactionOptions {
|
||||
* that callback settles it is revoked, and every later call fails without reaching PostgreSQL.
|
||||
*
|
||||
* Only the innermost open scope may run statements: while a savepoint is open, its parent refuses
|
||||
* statements and new savepoints. A callback must await every operation it starts before it returns;
|
||||
* one that settles with work outstanding is never committed or released.
|
||||
* statements and new savepoints. A callback must await every operation it starts before it returns.
|
||||
* One that resolves with work outstanding forgot an `await`: it fails with
|
||||
* `MODULE_DATABASE_SCOPE_WORK_OUTSTANDING` and is rolled back. One that rejects keeps its own error.
|
||||
* Either way, outstanding work never leads to a commit or a release.
|
||||
*
|
||||
* Transaction-control SQL (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `RELEASE SAVEPOINT`, …) is
|
||||
* forbidden through `query`; the callbacks control the transaction.
|
||||
*/
|
||||
export interface ModuleDatabaseTransaction extends ModuleDatabaseQueryable {
|
||||
/** Core names the savepoint. A resolved callback releases it; a rejected one rolls back to it. */
|
||||
/** Core names the savepoint. A resolved callback with no work outstanding releases it; a rejected
|
||||
* one rolls back to it and keeps its own error. */
|
||||
savepoint<T>(callback: (savepoint: ModuleDatabaseTransaction) => Promise<T> | T): Promise<T>;
|
||||
}
|
||||
|
||||
@@ -120,7 +126,9 @@ export interface ModuleDatabaseTransaction extends ModuleDatabaseQueryable {
|
||||
*/
|
||||
export interface ModuleDatabaseExecutor extends ModuleDatabaseQueryable {
|
||||
/** Core begins the transaction, commits when `callback` resolves with no work outstanding, and
|
||||
* rolls back otherwise. Core never retries it. */
|
||||
* rolls back otherwise; a rejected callback's own error is the transaction's error. Core never
|
||||
* retries it. These guarantees apply to conforming use: no transaction-control SQL through
|
||||
* `query`. */
|
||||
transaction<T>(
|
||||
callback: (transaction: ModuleDatabaseTransaction) => Promise<T> | T,
|
||||
options?: ModuleDatabaseTransactionOptions,
|
||||
@@ -135,7 +143,12 @@ export interface ModuleDatabaseExecutor extends ModuleDatabaseQueryable {
|
||||
export const MODULE_DATABASE_ERROR_CODES = [
|
||||
/** The call is not valid ABI use: a malformed query request or callback, a statement text that
|
||||
* holds no statement, an object-mode result with duplicate column names, or a transaction-control
|
||||
* statement sent through `query`. A programming error. */
|
||||
* statement sent through `query`. A programming error.
|
||||
*
|
||||
* The last two are only detectable once PostgreSQL has run the statement, so its effects may
|
||||
* already have happened: a mutation with duplicate result columns has been applied (inside a
|
||||
* transaction, it rolls back with it), and transaction control has taken effect, which Core cannot
|
||||
* undo. */
|
||||
"MODULE_DATABASE_REQUEST_INVALID",
|
||||
/** A parameter value is outside the ABI's parameter set. Refused before anything reaches
|
||||
* PostgreSQL. */
|
||||
@@ -154,8 +167,9 @@ export const MODULE_DATABASE_ERROR_CODES = [
|
||||
"MODULE_DATABASE_SCOPE_CLOSED",
|
||||
/** The executor's scope is open but not active, because a savepoint inside it is open. */
|
||||
"MODULE_DATABASE_SCOPE_INACTIVE",
|
||||
/** A transaction or savepoint callback settled while work it started was still outstanding. The
|
||||
* scope was rolled back, never committed or released. */
|
||||
/** A transaction or savepoint callback resolved while work it started was still outstanding: a
|
||||
* forgotten `await`. The scope was rolled back, never committed or released. (A callback that
|
||||
* rejects keeps its own error instead.) */
|
||||
"MODULE_DATABASE_SCOPE_WORK_OUTSTANDING",
|
||||
] as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user