diff --git a/docs/backlogs/000171-exact-family-lock-system-redesign.md b/docs/backlogs/000171-exact-family-lock-system-redesign.md
index 594926df..b6f290b1 100644
--- a/docs/backlogs/000171-exact-family-lock-system-redesign.md
+++ b/docs/backlogs/000171-exact-family-lock-system-redesign.md
@@ -10,9 +10,27 @@ Prerequisites docs/backlogs/000169-separate-session-operation-lock-scopes.md and
## Deferred From (Optional)
+docs/tasks/000249-runtime-owned-table-ddl.md;
+docs/rfcs/0026-engine-owned-mandatory-background-runtime.md Phase 2
## Deferral Context (Optional)
+- Defer Reason: Task 000249 must establish complete caller-prepared table-DDL
+ authority without widening RFC-0026 Phase 2 into the cross-cutting lock
+ representation redesign. The broader redesign should begin after the current
+ mandatory-runtime RFC phases establish their operation ownership patterns.
+- Findings: Accepted table DDL owns catalog metadata-S and data-IX claims under
+ its operation owner, while its nested private transaction performs the
+ catalog writes. Reacquiring those claims for the transaction is
+ correctness-safe through current same-family coverage, but duplicates lock
+ manager grants and owner-cache entries. `PreparedCatalogWriteAuthority`
+ therefore remains a narrow borrowed proof that reuses the operation claims
+ and preserves the no-reacquisition acceptance boundary.
+- Direction Hint: Unify operation and nested-transaction claims under one
+ exact-family authority model, then remove the special prepared catalog-write
+ path without adding duplicate grants, a generic lock-bypass flag, or another
+ ownership-transfer protocol. Preserve the prepared-statement panic settlement
+ introduced by task 000249 independently of the lock representation.
## Scope Hint
diff --git a/docs/benchmark-tool.md b/docs/benchmark-tool.md
index a3e593ac..e06d4c47 100644
--- a/docs/benchmark-tool.md
+++ b/docs/benchmark-tool.md
@@ -270,7 +270,7 @@ doradb-bench --root target/doradb-bench/index-scan run index-scan --num 10000 --
doradb-bench --root target/doradb-bench/index-scan cleanup
```
-## RFC-0025 Successful-Path Measurements
+## RFC-0025 and RFC-0026 Successful-Path Measurements
The new workloads complete the pre-RFC successful-path shapes needed by
RFC-0025:
@@ -278,7 +278,7 @@ RFC-0025:
- Phase 1/2 statement and transaction evidence uses `stmt-noop` and
`trx-noop`.
- Phase 2's no-per-item stream budget uses `index-stream`.
-- Phase 4's successful table-DDL path uses `table-ddl`.
+- RFC-0026 Phase 2's runtime-owned table-DDL path uses `table-ddl`.
- Phase 5's successful index-DDL path uses `index-ddl`.
- Existing insert, lookup, table-scan, and index-scan workloads remain the
row/index/page-loop evidence.
diff --git a/docs/engine-component-lifetime.md b/docs/engine-component-lifetime.md
index eb944fe8..e86f01b1 100644
--- a/docs/engine-component-lifetime.md
+++ b/docs/engine-component-lifetime.md
@@ -183,6 +183,18 @@ shutdown. Conversely, a prepared caller future retained without being polled
still owns its voluntary resources and can block shutdown until it resumes or
drops.
+`CREATE TABLE` and `DROP TABLE` are the first production users of this caller
+contract. The session validates input, reserves its DDL operation, and acquires
+the complete target/catalog lock set while the future is still cancellable.
+After caller capacity is available, synchronous acceptance transfers the
+operation entry, locks, immutable execution plan, and exact table runtime (for
+DROP) to the mandatory owner. The public future then waits only through the
+execution-inert observer. Normal completion drops settled progress, releases
+the prepared locks, and only then publishes the outer operation terminal.
+Unexpected execution unwind instead retains `FailedRetained`, publishes
+mandatory-runtime poison, and releases the caller permit after the accepted
+owner is dropped.
+
The supervisor catches both synchronous future construction and polling
unwinds while the accepted operation or cleanup job remains in an outer owner.
Its domain policy first releases or moves residual unsafe ownership into fatal
diff --git a/docs/lock-system.md b/docs/lock-system.md
index 9ee0ba85..fd431d8d 100644
--- a/docs/lock-system.md
+++ b/docs/lock-system.md
@@ -120,8 +120,8 @@ The current implementation uses the following table-level mapping:
| Explicit shared table lock | session `S` | session `S` | explicit session |
| Explicit exclusive table lock | session `S` | session `X` | explicit session |
| Freeze/checkpoint | scoped `S` | scoped `IS` | maintenance operation |
-| CREATE TABLE on a new id | scoped `X` | none | DDL operation |
-| DROP TABLE | scoped `X` | scoped `X` | DDL operation |
+| CREATE TABLE on a new id | target `X`; catalog slots 0-3 `S` | catalog slots 0-3 `IX` | prepared DDL operation, then mandatory owner |
+| DROP TABLE | target `X`; catalog slots 0-4 `S` | target `X`; catalog slots 0-4 `IX` | prepared DDL operation, then mandatory owner |
| CREATE/DROP INDEX | scoped `X` | scoped `X` | DDL operation |
On first touch, statement metadata protection is handed to the transaction
@@ -138,6 +138,15 @@ Successfully bound reads therefore retain metadata protection until transaction
commit or rollback. Repeated operations use the transaction binding and lock
cache.
+Table CREATE/DROP acquire their complete fixed lock sequences while the public
+session future is still cancellable. Winning mandatory capacity synchronously
+transfers the same `OwnerLockState` and operation owner to accepted execution;
+there is no release/reacquire window. Catalog statements receive a typed
+prepared-write authority that proves metadata S plus data IX for each catalog
+table and bypasses ordinary transaction lock acquisition. Index DDL,
+maintenance, and ordinary catalog statements continue through their existing
+lock-manager paths.
+
Recovery, purge, and no-transaction replay do not acquire logical locks. They
run at lifecycle boundaries where foreground lock owners do not exist. Logical
lock state is volatile and is never reconstructed from redo.
diff --git a/docs/public-error-audit.csv b/docs/public-error-audit.csv
index b11d7705..0de1575a 100644
--- a/docs/public-error-audit.csv
+++ b/docs/public-error-audit.csv
@@ -1,8 +1,6 @@
file,function_or_method,disclose_calls
doradb-storage/src/catalog/index.rs,create_index_for_session,29
doradb-storage/src/catalog/index.rs,drop_index_for_session,16
-doradb-storage/src/catalog/table.rs,create_table_for_session,11
-doradb-storage/src/catalog/table.rs,drop_table_for_session,12
doradb-storage/src/engine.rs,Engine::new_session,1
doradb-storage/src/engine.rs,Engine::try_shutdown,1
doradb-storage/src/engine.rs,bootstrap_inner,23
@@ -20,9 +18,9 @@ doradb-storage/src/session.rs,Session::checkpoint_table_with_wait,3
doradb-storage/src/session.rs,Session::cleanup_secondary_mem_indexes,3
doradb-storage/src/session.rs,Session::close,3
doradb-storage/src/session.rs,Session::create_index,1
-doradb-storage/src/session.rs,Session::create_table,1
+doradb-storage/src/session.rs,Session::create_table,4
doradb-storage/src/session.rs,Session::drop_index,1
-doradb-storage/src/session.rs,Session::drop_table,1
+doradb-storage/src/session.rs,Session::drop_table,4
doradb-storage/src/session.rs,Session::freeze_table,3
doradb-storage/src/session.rs,Session::list_table_ids,1
doradb-storage/src/session.rs,Session::lock_table,2
diff --git a/docs/rfcs/0026-engine-owned-mandatory-background-runtime.md b/docs/rfcs/0026-engine-owned-mandatory-background-runtime.md
index 00988160..03738fa5 100644
--- a/docs/rfcs/0026-engine-owned-mandatory-background-runtime.md
+++ b/docs/rfcs/0026-engine-owned-mandatory-background-runtime.md
@@ -1177,10 +1177,12 @@ focused validation.
only where one poll has materially unbounded work.
- Non-goals: Do not migrate index DDL, redesign table lifecycle/catalog
semantics, or parallelize one table DDL.
- - Task Doc: `docs/tasks/TBD.md`
- - Task Issue: `#0`
- - Phase Status: `pending`
- - Implementation Summary: `pending`
+ - Task Doc: `docs/tasks/000249-runtime-owned-table-ddl.md`
+ - Task Issue: `#924`
+ - Phase Status: done
+ - Implementation Summary: Implemented caller-prepared, mandatory-runtime-owned CREATE TABLE and DROP TABLE with complete operation lock scopes, nested private transactions, supervised completion and panic retention, deterministic cross-thread tests, and validated benchmark parity. [Task Resolve Sync: docs/tasks/000249-runtime-owned-table-ddl.md @ 2026-08-02]
+ - Related Backlogs:
+ - `docs/backlogs/000171-exact-family-lock-system-redesign.md`
- **Phase 3: Runtime-Owned Index DDL**
- Scope: Prepare create/drop index on the caller, including authoritative
diff --git a/docs/table-file.md b/docs/table-file.md
index 1e3faaad..009ce115 100644
--- a/docs/table-file.md
+++ b/docs/table-file.md
@@ -202,6 +202,18 @@ publication or enqueue handoff. Consequently `DROP TABLE` either closes a
reversible workflow immediately or asynchronously drains the publisher that
already won admission.
+Public table DDL owns only preparation. `CREATE TABLE` validates its schema and
+prepares all logical locks before mandatory acceptance; the runtime-owned task
+then creates the provisional file, starts the private catalog transaction,
+publishes the initial root, builds the runtime, and preserves the existing
+precommit compensation policy. `DROP TABLE` similarly transfers the exact
+current-live runtime and complete lock scope before it closes the lifecycle and
+waits for any admitted publisher. Dropping the public future after acceptance
+does not abandon either file workflow. Ordinary failures still compensate
+inside accepted CREATE execution, while a panic or unsafe post-gate DROP
+failure is retained and poisons storage rather than running fallible cleanup
+from a destructor.
+
### 7.1 Data Checkpoint Publication
Data checkpoint publishes:
diff --git a/docs/tasks/000249-runtime-owned-table-ddl.md b/docs/tasks/000249-runtime-owned-table-ddl.md
new file mode 100644
index 00000000..a89a0aa0
--- /dev/null
+++ b/docs/tasks/000249-runtime-owned-table-ddl.md
@@ -0,0 +1,1278 @@
+---
+id: 000249
+title: Runtime-Owned Table DDL
+status: implemented # proposal | implemented | superseded
+created: 2026-08-01
+github_issue: 924
+---
+
+# Task: Runtime-Owned Table DDL
+
+## Summary
+
+Implement Phase 2 of RFC-0026 by moving accepted `CREATE TABLE` and
+`DROP TABLE` execution from the caller executor to the engine-owned mandatory
+runtime. Keep caller-owned preparation in `session.rs`: validate public input,
+reserve one DDL session operation, acquire the complete target and catalog
+logical-lock scope, and wait for mandatory capacity while that preparation
+remains cancellable. Keep the prepared and accepted execution implementations,
+effectful catalog/file/table-lifecycle work, compensation, and result policy in
+the catalog table module.
+
+Use one operation-owned, lifetime-free lock scope across the acceptance
+boundary. Start the private catalog transaction only after acceptance, and
+teach catalog statements to consume a typed proof of already-prepared catalog
+write authority without reacquiring metadata/data locks. Extend the stable
+session-operation entry so a mandatory operation can own and settle its nested
+private transaction before releasing operation locks and publishing terminal
+state.
+
+Preserve the existing CREATE rollback/file/runtime compensation boundaries and
+DROP irreversible-gate poison policy. Before acceptance, caller cancellation
+releases every partial or complete preparation grant. After acceptance, caller
+or completion-observer drop is execution-inert. Only
+`AcceptedExecution::execute` may unwind; `finish`, `handle_panic`, and resource
+release are infallible and non-panicking as required by the Phase 1 mandatory
+runtime contract.
+
+## Context
+
+`Issue Labels:`
+`- type:task`
+`- priority:medium`
+`- codex`
+
+`Parent RFC:`
+`- docs/rfcs/0026-engine-owned-mandatory-background-runtime.md`
+
+This task is RFC-0026 Phase 2, **Runtime-Owned Table DDL**. Phase 1 is complete
+through task `000248` and issue `#922`; it provides:
+
+- the fixed engine-owned mandatory executor and caller-capacity admission;
+- the consuming `PreparedExecution -> AcceptedExecution` handoff;
+- exclusive completion producer/observer ownership;
+- panic supervision with the accepted owner outside the caught future;
+- compact `Voluntary` and `Mandatory` session-operation states;
+- concurrent transaction cleanup and ordered shutdown drain.
+
+The Phase 2 prerequisite remains an implementation gate: focused Phase 1
+acceptance, panic, cleanup, and shutdown tests must pass with both one and
+multiple mandatory runner threads before table DDL integration is considered
+complete.
+
+Phase 2 resolves its local catalog-authority choice with a narrow owned
+operation-lock scope plus a typed prepared-catalog-write capability. It does
+not begin a private transaction before capacity and does not adopt a
+transaction-owned lock state across acceptance. This avoids retaining an
+active transaction timestamp while prepared work waits for runtime capacity
+and avoids a second lock-owner transfer protocol.
+
+The approved source boundary is:
+
+- `session.rs` owns `prepare_create_table` and `prepare_drop_table`, complete
+ caller-side lock acquisition, mandatory submission, and observer waiting;
+- `catalog/table.rs` owns create/drop execution plans, the four
+ prepared/accepted execution carriers, phase progress, compensation, and
+ catalog-specific invariants;
+- the current monolithic `create_table_for_session` and
+ `drop_table_for_session` functions are removed;
+- `SessionDdlContext` remains for index DDL until RFC-0026 Phase 3.
+
+The current `Session::create_table` path validates user primary-key policy and
+table metadata, atomically allocates a table ID, acquires only target metadata
+`X`, creates a provisional file, starts a private transaction, stages four
+catalog tables, publishes the file/root, builds the runtime, commits, and
+installs the runtime on the caller executor.
+
+The current `Session::drop_table` path acquires target metadata/data `X`,
+validates both the current runtime and catalog row, starts a private
+transaction, closes and drains the table lifecycle, stages a five-table
+catalog cascade, commits, retains the dropped runtime, and requests purge on
+the caller executor. `DropTableProgressGuard::drop` poisons if that future is
+abandoned after the lifecycle gate.
+
+Both catalog staging paths currently call transaction statement helpers that
+acquire catalog-table metadata `S` and data `IX` after the target DDL locks.
+Those hidden operation-lock awaits cannot remain in mandatory execution.
+`FreshLockGuard` and `ScopedTableDdlLocks` borrow `&LockManager`, so neither can
+cross the required `'static` accepted-task boundary.
+
+The existing create-table allocator is an `AtomicU64::fetch_add` with a
+monotonic user/catalog namespace boundary. Checkpoint metadata persists the
+next ID, recovery advances it from recovered CREATE records, and recovery
+removes table files absent from recovered current or retained-drop state.
+Allocated gaps are allowed. Therefore preparation does not probe current
+runtime or catalog storage for a duplicate CREATE ID. An impossible catalog
+primary-key or runtime-map duplicate is an execution invariant, not a
+recoverable preparation outcome.
+
+Public user-table primary-key rejection is a different contract and remains.
+Task `000206` deliberately keeps `IndexAttributes::PK` internal to catalog
+table definitions and requires public `CREATE TABLE` and `CREATE INDEX` to
+reject it. This pure input validation is performed before operation
+reservation and creates no runtime effect.
+
+DROP receives an arbitrary external table ID and must still distinguish
+`TableNotFound` while retaining the exact current `Arc
`. It therefore
+performs one current-live runtime lookup after target exclusion. It does not
+perform a second catalog-row lookup, a duplicate foreground-live check, or a
+separate post-lock health check. Missing catalog rows after a current-live
+runtime has been selected are engine invariants handled by the execution-side
+catalog cascade and mandatory panic policy.
+
+The immediately following RFC phase, Phase 3 runtime-owned index DDL, assumes
+this task establishes the production session wrapper, operation-entry
+transition, typed completion/error observation, prepared catalog authority,
+and deterministic cross-thread gate-testing pattern. This task must leave that
+assumption intact without migrating index DDL itself.
+
+Relevant design and implementation sources:
+
+- `docs/rfcs/0026-engine-owned-mandatory-background-runtime.md`
+- `docs/tasks/000248-mandatory-operation-driver-and-concurrent-cleanup-executor.md`
+- `docs/tasks/000206-catalog-primary-key-contract.md`
+- `docs/architecture.md`
+- `docs/engine-component-lifetime.md`
+- `docs/transaction-system.md`
+- `docs/lock-system.md`
+- `docs/table-file.md`
+- `docs/process/coding-guidance.md`
+- `docs/process/unit-test.md`
+- `doradb-storage/src/runtime/mandatory.rs`
+- `doradb-storage/src/session.rs`
+- `doradb-storage/src/catalog/table.rs`
+- `doradb-storage/src/catalog/mod.rs`
+- `doradb-storage/src/catalog/storage/{tables,columns,indexes,table_replay_silent_watermarks}.rs`
+- `doradb-storage/src/trx/{mod,stmt}.rs`
+- `doradb-storage/src/lock/{mod,state}.rs`
+- `doradb-storage/src/error.rs`
+
+## Goals
+
+1. Split public CREATE/DROP into caller-owned preparation followed by
+ runtime-owned accepted execution at the first operation-effect boundary.
+2. Keep preparation orchestration in `session.rs` and catalog-specific
+ prepared/accepted execution implementations in catalog modules.
+3. Acquire every target and catalog table-level logical lock required by the
+ accepted call graph before mandatory capacity admission.
+4. Transfer one owned `LockManager` guard and exact `OwnerLockState` across
+ acceptance; release partial preparation cleanly on error or caller drop.
+5. Prove that accepted CREATE/DROP performs no `LockManager` acquisition or
+ operation-lock reacquisition.
+6. Start the private catalog transaction only after acceptance and represent
+ its available, running, completing, empty, and fatal-retained states inside
+ the enclosing `Mandatory` session operation.
+7. Preserve existing public metadata validation, user-PK rejection,
+ `TableNotFound`, explicit-session-lock conflict, and typed error semantics
+ without adding redundant ID or catalog-row preflight reads.
+8. Preserve CREATE precommit compensation, post-root/commit poison, and
+ postcommit runtime-install policy.
+9. Preserve DROP pre-gate rollback, irreversible lifecycle drain, post-gate
+ poison, dropped-runtime retention, and purge requests.
+10. Make observer drop after acceptance semantically inert at every execution
+ phase, including when the public session future is abandoned.
+11. Keep progress and operation resources in the accepted owner outside the
+ unwind-caught execution future so panic supervision can publish
+ `FailedRetained`, poison, and release safe logical resources.
+12. Ensure only accepted execution may unwind. Make normal finish, panic
+ handling, progress/lock Drop, and terminal resource release contain no
+ panicking assertions or fallible domain cleanup.
+13. Release or safely retain all nested transaction and progress ownership
+ before releasing operation locks, and release locks before normal session
+ terminal publication.
+14. Provide deterministic, engine-scoped, cross-thread tests for preparation,
+ capacity, acceptance, every reversible/irreversible execution phase,
+ panic, final release, and shutdown.
+15. Establish the production integration pattern required by RFC-0026 Phase 3
+ without adding successful transaction/statement hot-path work.
+
+## Non-Goals
+
+1. Do not migrate `CREATE INDEX` or `DROP INDEX`; those remain RFC-0026
+ Phase 3.
+2. Do not migrate checkpoint, redo truncation, index cleanup, or other
+ maintenance; those remain later RFC phases.
+3. Do not redesign table metadata, user primary-key support, table-ID
+ allocation, catalog schema, catalog redo, table files, recovery format, or
+ dropped-table retention semantics.
+4. Do not remove public user-table `IndexAttributes::PK` rejection or weaken
+ static catalog primary-key validation.
+5. Do not add a CREATE ID-existence query, catalog primary-key preflight, or
+ DROP catalog-row preflight merely to recheck allocator/catalog invariants.
+6. Do not bypass the catalog MVCC engine's inherent unique-index enforcement
+ or existing row/key shape validation.
+7. Do not redesign `LockManager`, add a generic prepared-lock plan API, add
+ deadlock detection, add lock leases, or revoke a retained caller
+ preparation.
+8. Do not transfer a pre-acceptance private transaction or transaction lock
+ owner into the runtime.
+9. Do not add mandatory scheduler priorities, adaptive capacity, work
+ stealing, a task registry, or domain-specific DDL workers.
+10. Do not parallelize one CREATE/DROP operation or add speculative
+ cooperative yields where existing awaits already bound a poll.
+11. Do not retry DDL automatically, reopen a table after the DROP lifecycle
+ gate, or reinterpret existing ordinary/fatal failures.
+12. Do not run fallible compensation from `handle_panic` or any Drop
+ implementation after an arbitrary unwind.
+13. Do not modify historical completed task documents. RFC-0025 is already
+ explicitly superseded; update it only if implementation finds a remaining
+ normative statement that conflicts with RFC-0026.
+14. Do not change `.config/nextest.toml` or introduce a second test runner or
+ timeout policy.
+
+## Plan
+
+### 1. Split the public session call paths at the first operation effect
+
+Refactor `Session::create_table` to follow this sequence:
+
+```text
+pure TableSpec/IndexSpec validation
+ -> reserve SessionOperationKind::Ddl
+ -> allocate gap-tolerant TableID
+ -> build owned CreateTablePlan
+ -> acquire complete PreparedTableDdlLocks
+ -> construct catalog::PreparedCreateTable
+ -> await mandatory caller capacity
+ -> synchronous accept and detached runtime spawn
+ -> drop caller mandatory-runtime guard
+ -> await CompletionObserver
+```
+
+Refactor `Session::drop_table` to follow:
+
+```text
+reject non-user ID
+ -> reserve SessionOperationKind::Ddl
+ -> reject same-session explicit target lock
+ -> acquire complete PreparedTableDdlLocks
+ -> resolve exact current-live Arc
under target exclusion
+ -> construct catalog::PreparedDropTable
+ -> await mandatory caller capacity
+ -> synchronous accept and detached runtime spawn
+ -> drop caller mandatory-runtime guard
+ -> await CompletionObserver
+```
+
+Add private `prepare_create_table` and `prepare_drop_table` helpers in
+`session.rs`. They own sequencing and cancellation. Catalog code may expose
+pure constructors for catalog-specific plan objects and fixed catalog write
+target lists, but it must not reacquire the session operation or drive
+caller-side preparation.
+
+Remove `create_table_for_session` and `drop_table_for_session`. Keep
+`create_index_for_session`, `drop_index_for_session`, and `SessionDdlContext`
+unchanged except for imports or shared helper movement that is mechanically
+required.
+
+Clone the mandatory-runtime access guard from the pinned engine before moving
+the session operation into the prepared carrier. Once `submit` returns the
+observer, explicitly drop that caller guard before `observer.wait()`. The
+mandatory task and permit retain their own runtime guards; the observer must
+not become an engine/runtime lifetime authority.
+
+### 2. Keep only necessary preparation validation
+
+CREATE pure validation remains before operation reservation:
+
+- reject user-supplied primary-key index attributes;
+- validate column/index shape, referenced column numbers, empty keys,
+ nullability, duplicate PK metadata, and other existing
+ `TableMetadata::try_new` rules;
+- construct `Arc` and catalog row objects from owned input.
+
+Allocate the table ID only after pure input succeeds. Cancellation after ID
+allocation may leave a gap. This is intentional and already compatible with
+checkpoint/recovery allocator semantics.
+
+Do not query the current runtime map, metadata history, or `catalog.tables`
+under the new target lock. The allocator provides uniqueness. Keep target
+metadata `X` because it protects the newly published runtime from admission
+until CREATE has settled its nested transaction and completed final lock
+release, not because preparation expects an ID collision.
+
+Do not add an explicit catalog primary-key uniqueness scan. Catalog insert
+continues using its ordinary unique-index mutation. Any impossible duplicate
+reported by the catalog mutation remains an invariant assertion inside
+accepted execution and is caught by mandatory panic supervision.
+
+DROP validation remains deliberately asymmetric because its table ID is
+caller-supplied:
+
+- reject catalog/out-of-range IDs before reserving an operation;
+- reject an explicit target lock held by the same session before waiting;
+- after complete target exclusion, call the synchronous current-live catalog
+ runtime lookup exactly once;
+- return `OperationError::TableNotFound` when absent and otherwise retain that
+ exact `Arc
` in `DropTablePlan`.
+
+Do not call `ensure_user_table_catalog_row` during DROP preparation. Do not add
+a second `check_foreground_live`; the current-live map plus target exclusion
+selects the authoritative target, while `start_drop_lifecycle` remains the
+execution-side transition. Do not add a separate health check after the lock
+wait; `mandatory::submit` owns the health/capacity race and releases the
+prepared carrier if admission is closed or poisoned.
+
+Keep existing catalog row/value and primary-key shape validation inside
+catalog statement mutation. Those checks validate trusted write construction
+against the static catalog schema; they are not an existence preflight and
+are outside this task's redundant-read removal.
+
+### 3. Add one lifetime-free prepared table-DDL lock scope
+
+Add the following crate-private session-owned shapes, with final naming allowed
+to follow local style:
+
+```rust
+pub(crate) struct PreparedTableDdlLocks {
+ lock_manager: QuiescentGuard,
+ locks: OwnerLockState,
+}
+
+pub(crate) struct PreparedTableDdlScope {
+ operation: Option,
+ locks: Option,
+}
+
+pub(crate) struct AcceptedTableDdlScope {
+ operation: MandatoryOperationGuard,
+ locks: Option,
+ finish_state: TableDdlFinishState,
+}
+```
+
+`PreparedTableDdlLocks` clones the component-owned lock-manager guard and
+creates `OwnerLockState` with `SessionOperationPin::operation_lock_owner()`.
+Acquisition records each successfully granted resource immediately. If the
+current awaited request is cancelled, the lock manager's waiter guard removes
+it; if any later acquisition or preparation step fails, dropping the owned
+scope releases all previously recorded grants.
+
+Its Drop implementation calls only the existing idempotent
+`OwnerLockState::release_all` path. It must not assert the release count, call
+`assert_cleared`, format an invariant report, poison, or perform fallible
+cleanup. Exact release behavior is verified by tests outside Drop.
+
+`PreparedTableDdlScope` explicitly drops/takes the lock scope before the
+foreground `SessionOperationPin` so preparation release order is:
+
+```text
+cancel current waiter
+ -> release every granted operation lock
+ -> publish foreground operation release/Terminal
+```
+
+Catalog table code exposes fixed write-target slices derived from the catalog
+tables it actually mutates:
+
+- CREATE: `tables`, `columns`, `indexes`, `index_columns`;
+- DROP: the same four plus `table_replay_silent_watermarks`.
+
+Acquire requests in canonical `LockResource` order:
+
+CREATE, 9 grants:
+
+1. target user-table `TableMetadata(table_id)` in `X`;
+2. catalog slots 0 through 3 `TableMetadata` in ascending ID order, each `S`;
+3. catalog slots 0 through 3 `TableData` in ascending ID order, each `IX`.
+
+DROP, 12 grants:
+
+1. target user-table `TableMetadata(table_id)` in `X`;
+2. catalog slots 0 through 4 `TableMetadata` in ascending ID order, each `S`;
+3. target user-table `TableData(table_id)` in `X`;
+4. catalog slots 0 through 4 `TableData` in ascending ID order, each `IX`.
+
+User table IDs are below catalog table IDs, so these sequences obey the global
+metadata-before-data and ascending-ID rule. Do not allocate or expose a
+general-purpose lock-plan abstraction for these two fixed lists.
+
+### 4. Make capacity admission and acceptance a zero-await ownership edge
+
+Implement these catalog-owned carriers:
+
+```rust
+pub(crate) struct PreparedCreateTable {
+ scope: PreparedTableDdlScope,
+ plan: CreateTablePlan,
+ metadata: MandatoryTaskMetadata,
+}
+
+pub(crate) struct AcceptedCreateTable {
+ scope: AcceptedTableDdlScope,
+ progress: CreateTableProgress,
+}
+
+pub(crate) struct PreparedDropTable {
+ scope: PreparedTableDdlScope,
+ plan: DropTablePlan,
+ metadata: MandatoryTaskMetadata,
+}
+
+pub(crate) struct AcceptedDropTable {
+ scope: AcceptedTableDdlScope,
+ progress: DropTableProgress,
+}
+```
+
+Implement `PreparedExecution` for both prepared types and
+`AcceptedExecution` for both accepted types. Use stable labels
+`create_table` and `drop_table`, the exact session operation key, and table ID
+in immutable mandatory metadata.
+
+All vectors, metadata, task diagnostics, and phase containers required to
+construct the accepted value must already be allocated before capacity wins.
+`PreparedExecution::accept` only destructures owned fields, calls the Phase 1
+consuming `SessionOperationPin::into_mandatory` transition, and constructs the
+accepted value. It contains no await, error return, test panic hook, catalog
+lookup, file operation, transaction begin, lock acquisition, or expected
+rejection.
+
+The current Phase 1 handoff uses `unwrap`/`assert` to re-resolve and validate
+the active entry even though `SessionOperationPin` already owns that exact
+entry and no nested transaction is permitted before acceptance. Production
+table DDL makes acceptance non-panicking: use the retained entry and exclusive
+pin as the ownership proof, perform the direct
+`Voluntary(None) -> Mandatory(None)` transition while holding the required
+lifecycle mutex, and remove panic-capable relookup from this accepted adapter.
+Preserve the lock-order and lifecycle notification behavior. Prove the state
+precondition with type/ownership construction and focused tests rather than an
+assertion outside the supervised execution future.
+
+Capacity saturation retains `PreparedCreateTable` or `PreparedDropTable` and
+the complete lock scope in the caller future. It does not consume a mandatory
+permit or create a detached task until capacity succeeds. Dropping that caller
+future releases the preparation normally. A retained but unpolled future may
+retain its locks and keep shutdown busy, as documented by RFC-0026.
+
+### 5. Add typed prepared catalog-write authority
+
+Introduce a narrow borrowed capability in the transaction statement layer,
+approximately:
+
+```rust
+pub(crate) struct PreparedCatalogWriteAuthority<'a> {
+ locks: &'a OwnerLockState,
+}
+```
+
+Only an accepted prepared-operation lock scope creates this view. Its
+catalog-table write assertion checks the authoritative owner-local cache for:
+
+- `TableMetadata(catalog_table_id)` covered by `S`;
+- `TableData(catalog_table_id)` covered by `IX`.
+
+Do not re-read the lock manager after acceptance. The owned lock state is the
+proof and the sole release record. A missing prepared grant is an internal
+execution invariant and may assert only while the accepted `execute` future is
+inside the mandatory unwind boundary.
+
+Add a private transaction entry such as
+`Transaction::stage_prepared_catalog_statement(authority, callback)`.
+`StmtState`/`Statement` may carry an optional borrowed capability for the
+duration of that private statement. Catalog insert and primary-key delete
+then:
+
+1. when prepared authority is present, assert exact table coverage and skip
+ `acquire_table_write_metadata_lock` and
+ `acquire_table_write_data_lock`;
+2. otherwise use the current lock-aware path unchanged;
+3. preserve existing DML shape/key validation, statement effects, undo, redo,
+ rollback, and catalog error narrowing.
+
+Use the prepared entry only from accepted table DDL. Existing foreground index
+DDL, maintenance, catalog tests, and ordinary transaction statements continue
+through `stage_catalog_statement` and acquire their normal transaction locks.
+Do not add a public or generic boolean `skip_locks` flag.
+
+Logical table locks are completely prepared. Row undo/CDB ownership, page/tree
+latches, IO completion, redo/group commit, lifecycle drain, and other
+execution-internal synchronization remain allowed after acceptance because
+they are not hidden `LockManager` operation authority.
+
+### 6. Extend mandatory operations to own a nested private transaction
+
+Factor private-transaction construction so both `SessionOperationPin` and
+`MandatoryOperationGuard` can start it with the exact operation key, kind,
+session state, engine, and stable entry. The table DDL adapter calls
+`MandatoryOperationGuard::begin_private_trx` only from accepted execution.
+
+Extend every relevant `SessionOperationEntry` transition exhaustively:
+
+```text
+Voluntary(None)
+ -- accept_mandatory -->
+Mandatory(None)
+ -- install private transaction -->
+Mandatory(Some(Available))
+ -- statement checkout -->
+Mandatory(Some(Running))
+ -- ordinary statement return -->
+Mandatory(Some(Available))
+ -- commit/rollback terminal claim -->
+Mandatory(Some(Completing))
+ -- matching transaction finish -->
+Mandatory(None)
+```
+
+Preserve the equivalent `Voluntary(Some(...))` behavior for unmigrated index
+DDL and maintenance.
+
+Update:
+
+- `install_private_transaction`;
+- `take_for_checkout`;
+- `return_inner`;
+- `take_for_terminal`;
+- `take_for_cleanup` where an already-owned terminal path requires it;
+- `finish_transaction`;
+- shutdown/inspection labels and exhaustive matches;
+- focused transition tests.
+
+An accepted private transaction is owned by the mandatory execution. Normal
+error paths explicitly commit or roll it back before returning. Dropping its
+`Transaction` handle while the operation is `Mandatory` must not submit a
+competing abandoned-transaction cleanup. An arbitrary execution unwind is
+instead preserved through the accepted panic policy and
+`FailedRetained`. Shutdown must not claim a nested cleanup out from under the
+accepted task.
+
+Successful nested commit or rollback returns to `Mandatory(None)`, never
+directly to outer `Terminal`. Outer terminal publication is reserved until
+progress resources and operation locks are released.
+
+### 7. Represent every execution effect in accepted progress
+
+Construct catalog-specific owned plans before acceptance:
+
+```rust
+pub(crate) struct CreateTablePlan {
+ table_id: TableID,
+ metadata: Arc,
+ table_object: TableObject,
+ column_objects: Vec,
+ index_objects: Vec,
+ index_column_objects: Vec,
+}
+
+pub(crate) struct DropTablePlan {
+ table_id: TableID,
+ table: Arc
,
+}
+```
+
+Evolve `CreateTableProgress` so it exists before the provisional file and owns
+options for every resource that may survive an await or unwind:
+
+- immutable plan;
+- phase;
+- mutable/provisional table file;
+- private `Transaction`;
+- published `Arc`;
+- staged `Arc
`;
+- commit timestamp until installation.
+
+Use phases equivalent to:
+
+```text
+Prepared
+FileCreated
+PrivateTransactionActive
+CatalogStaged
+FilePublished
+RuntimeBuilt
+CatalogCommitted
+Installed | Aborted
+```
+
+Add an owned `DropTableProgress` containing the plan/table, phase, optional
+private transaction, and any retained terminal values needed across awaits:
+
+```text
+Prepared
+PrivateTransactionActive
+LifecycleClosed
+DrainComplete
+CatalogStaged
+CatalogCommitted
+RuntimeRetained
+```
+
+Progress methods may use invariant assertions only when invoked from
+`AcceptedExecution::execute`. Progress Drop must contain no assertion,
+debug assertion, poison call, fallible cleanup, or phase-dependent ownership
+decision.
+
+### 8. Execute and compensate CREATE inside the mandatory runtime
+
+The accepted CREATE sequence is:
+
+```text
+execution test hook before first effect
+ -> create provisional table file
+ -> retain file in CreateTableProgress
+ -> begin mandatory-nested private transaction
+ -> stage four catalog tables with prepared authority
+ -> publish the table-file root
+ -> build the user-table runtime
+ -> commit catalog DDL
+ -> install the current-live runtime
+ -> prove nested Mandatory(None)
+```
+
+The provisional file remains the first operation effect. No file creation,
+catalog mutation, transaction begin, lifecycle transition, or runtime
+publication occurs in caller preparation.
+
+Preserve the current compensation matrix:
+
+- file creation failure: return the typed runtime/IO failure; no catalog
+ transaction exists;
+- private transaction begin failure after file creation: delete the
+ provisional file inside `execute`;
+- catalog staging, file publication, runtime build, or injected precommit
+ failure: settle statement effects, roll back the private transaction,
+ destroy any staged runtime, and delete the provisional file;
+- cleanup failure before commit: preserve the cleanup/fatal policy and poison
+ where the existing workflow does, without replacing a stronger fatal
+ reason;
+- catalog commit failure after table-root publication: destroy the staged
+ runtime, poison, and retain the file for diagnosis/recovery;
+- successful commit followed by a failed current-runtime map insertion:
+ assert the impossible duplicate inside `execute`; mandatory panic policy
+ poisons and retains the operation instead of reporting an ordinary ID
+ conflict;
+- success: install the runtime, move owned values to their terminal
+ destinations, and request normal final publication.
+
+Keep all fallible deletion, rollback, runtime destruction, and poison-source
+selection inside `execute`, even when handling an ordinary error. `finish` and
+Drop are not compensators.
+
+### 9. Execute and retain DROP inside the mandatory runtime
+
+The accepted DROP sequence is:
+
+```text
+begin mandatory-nested private transaction
+ -> start_drop_lifecycle
+ -> await foreground/runtime publication drain
+ -> stage five-table catalog cascade with prepared authority
+ -> commit catalog DDL
+ -> publish dropped-runtime/replay-floor retention
+ -> request dropped-table and metadata-history purge
+ -> prove nested Mandatory(None)
+```
+
+Preserve the current policy:
+
+- private transaction begin failure: no lifecycle effect;
+- `start_drop_lifecycle` failure: roll back the private transaction and return
+ the operation error without poisoning;
+- after lifecycle close, catalog cascade failure: best-effort rollback inside
+ `execute`, retain the original cascade error as the poison source, and never
+ reopen the table;
+- after lifecycle close, commit failure: poison with the current
+ Runtime-or-Fatal source;
+- dropped-runtime retention failure: poison;
+- success: preserve the effective replay floor, retained runtime, and both
+ purge requests.
+
+The catalog cascade continues asserting that its current-live target row
+exists. That assertion is now explicitly an accepted-execution invariant:
+preparation does not scan the row merely to turn corruption into
+`TableNotFound`.
+
+Remove `DropTableProgressGuard`. Caller or observer cancellation can no longer
+abandon accepted execution, and panic policy is centralized in
+`AcceptedDropTable::handle_panic`.
+
+### 10. Preserve typed errors across completion
+
+Preparation errors remain in native Operation, Runtime, Lifecycle, or Fatal
+domains and are disclosed only at the public `Session` boundary before
+submission.
+
+Accepted execution returns `CompletionResult` directly. Add only the narrow
+crate-private conversions required to turn existing source-bearing
+Operation/Runtime/Fatal carrier arms into `CompletionErrorBridge`; do not
+capture or reconstruct the public `Error` wrapper and do not collapse a
+stronger Fatal reason into Runtime.
+
+The completion observer remains the sole move-once consumer. Waiting discloses
+the canonical typed report to the public caller. Dropping the observer marks
+the result unobserved but does not touch the accepted operation. The mandatory
+runtime logs unobserved ordinary failures and retains/poisons before publishing
+fatal completion according to its Phase 1 policy.
+
+### 11. Enforce an execution-only panic boundary
+
+Follow the Phase 1 `AcceptedExecution` contract literally:
+
+- only construction/polling of `execute` is inside `catch_unwind` and may
+ panic;
+- `finish` and `handle_panic` run outside that catch and must not unwind;
+- after either starts resource settlement, there is no second recovery
+ protocol.
+
+Move or keep all phase assertions, invariant assertions, deliberate panic test
+hooks, and panic-capable invariant ownership conversions inside `execute`.
+Specifically:
+
+- remove the current `CreateTableProgress::drop` `debug_assert`;
+- remove the DROP progress guard that poisons from Drop;
+- do not put `assert`, `debug_assert`, `unwrap`, `expect`, panic hooks, or
+ fallible domain cleanup in the new lock/scope/progress Drop paths;
+- do not assert released lock counts during `finish` or Drop;
+- do not perform file deletion, rollback, runtime destruction, catalog work,
+ or lifecycle transitions from `handle_panic`.
+
+Audit every RAII value that can be dropped while the caught execution future
+is unwinding, not only fields of the outer accepted carrier. In particular,
+the current private `StmtState::PrivateMustComplete` Drop assertion,
+`SessionOperationCheckout::return_inner` assertions, terminal
+`SessionOperationCompletionClaim`, and assertion-bearing `PreparedTrx` Drop
+cannot be left on an accepted table-DDL unwind path that could double-panic or
+drop rollback ownership.
+
+Add a mandatory prepared-catalog statement panic-settlement path. Catch a
+callback unwind while the `StmtState` owner is still structurally available,
+disarm its must-complete Drop policy, clear partial statement redo, fold
+residual row/index undo into the nested transaction core, release statement
+locks, and return that core directly to a `FailedRetained` mandatory entry
+through a non-panicking retention method; then resume the original unwind so
+the outer mandatory supervisor handles it. Normal statement success and typed
+error behavior remain unchanged.
+
+Likewise, audit the private transaction terminal edge before calling the
+existing commit/rollback machinery. Any active completion claim or prepared
+transaction that can still own rollback-relevant state across an await must
+either remain in an accepted/transaction-system owner outside the caught
+borrowed future or already have crossed an existing supervised, non-lossy
+handoff. Do not allow an active `TrxInner`, undo payload, lock state, or
+terminal attachment to be destroyed by an assertion-bearing Drop during
+unwind. Reuse the Phase 1 completion-claim and failed-precommit retention
+patterns; do not introduce a second generic supervisor.
+
+Split normal mandatory terminal validation from non-panicking publication.
+Use a private finish state equivalent to:
+
+```rust
+enum TableDdlFinishState {
+ Executing,
+ TerminalReady,
+ FailedRetained,
+}
+```
+
+At the common normal execution epilogue, verify that the private transaction is
+gone and the stable entry is exactly `Mandatory(None)`. This verification may
+assert because it is still inside `execute`. Transition the accepted scope to
+`TerminalReady` only after that validation succeeds.
+
+`AcceptedExecution::finish` then uses only non-panicking actions:
+
+```text
+drop/take already-settled progress owners
+ -> release PreparedTableDdlLocks
+ -> require the already-established TerminalReady state
+ -> publish outer Terminal and registry removal
+```
+
+Refactor the current assertion-bearing `MandatoryOperationGuard::finish` into
+an execution-side validation operation and a state-gated terminal transition
+that cannot panic. If `finish` defensively observes `Executing`, publish fatal
+retention/poison through a non-panicking fallback rather than asserting or
+exposing the session as idle.
+
+On an `execute` unwind, `handle_panic`:
+
+1. records immutable operation kind/key/table ID/last phase through the
+ existing mandatory metadata/diagnostic path;
+2. calls the non-panicking mandatory `fail_retained` transition;
+3. marks the accepted finish state `FailedRetained`;
+4. returns the canonical `MandatoryTaskPanic` completion bridge.
+
+It does not guess whether arbitrary partially completed work can be reversed.
+The generic supervisor then poisons the mandatory runtime and completes the
+observer. When the accepted carrier is subsequently dropped, its disarmed
+mandatory guard and progress owners are inert and its operation lock scope
+uses only idempotent release. The caller permit is released after the accepted
+owner drops. A retained nested transaction core stays in the stable
+`FailedRetained` entry so shutdown continues to observe the unsafe residual.
+
+### 12. Replace thread-local DDL hooks with deterministic cross-thread hooks
+
+The existing CREATE failure hook is thread-local and cannot control execution
+that resumes on a mandatory runner. Replace it with one test-only,
+engine-scoped controller shared by the session preparation and catalog
+execution paths. Keep parallel test engines isolated; do not use one
+process-global mutable phase selector without an existing serialization guard.
+
+Use events, channels, barriers, and explicit phase acknowledgements. Do not
+use wall-clock sleeps as evidence. Cover hooks around:
+
+- before/after each target and catalog lock request/grant;
+- partial preparation release;
+- complete preparation before capacity;
+- capacity waiting and winning;
+- immediately before and after synchronous acceptance;
+- accepted before first effect;
+- provisional file creation;
+- private transaction begin;
+- catalog staging;
+- file/root publication;
+- runtime build;
+- catalog commit;
+- CREATE runtime installation;
+- DROP lifecycle close;
+- DROP drain wait/completion;
+- DROP retained-runtime publication;
+- normal final lock release and outer terminal publication.
+
+Failure hooks return typed failures from `execute`. Panic hooks exist only at
+accepted execution phases. Preparation, `accept`, `finish`, `handle_panic`,
+and Drop hooks may block or observe where appropriate but must never inject a
+panic.
+
+Instrument lock-manager acquisition in tests so an accepted operation can
+assert that every request occurred before acceptance. A transaction-owner
+metadata/data request after acceptance must fail the test rather than merely
+eventually succeeding.
+
+Add explicit cooperative yields only if measurement shows a single poll does
+materially unbounded synchronous work. Existing file/transaction/catalog/drain
+awaits already provide scheduling points; do not add unconditional yield
+overhead speculatively.
+
+### 13. Update documentation, RFC phase state, and measurements
+
+Update current documentation to describe table DDL as caller-prepared and
+mandatory-runtime-owned after acceptance:
+
+- `docs/engine-component-lifetime.md`: add the concrete production table-DDL
+ use of the Phase 1 acceptance/observer contract;
+- `docs/transaction-system.md`: document
+ `Mandatory(Some(InternalTrxState))`, nested completion back to
+ `Mandatory(None)`, and `FailedRetained`;
+- `docs/lock-system.md`: replace stale nested foreground-DDL cancellation and
+ future handoff wording for table DDL while leaving index/maintenance scope
+ explicit;
+- `docs/table-file.md`: audit CREATE provisional-file and DROP lifecycle
+ ownership wording for caller-versus-runtime accuracy;
+- `docs/benchmark-tool.md`: map `table-ddl` to RFC-0026 Phase 2 rather than the
+ superseded RFC-0025 Phase 4 plan.
+
+Audit RFC-0025 and legacy tests for foreground-driver/handoff assumptions.
+RFC-0025 already states that RFC-0026 controls post-Phase-2 execution design,
+so avoid historical churn when no normative conflict remains.
+
+At `$task-resolve`, synchronize RFC-0026 Phase 2:
+
+- Task Doc: `docs/tasks/000249-runtime-owned-table-ddl.md`;
+- Task Issue: the created issue number when available;
+- Phase Status: `done`;
+- a concise implementation summary with the resolve-sync marker;
+- any related backlog produced by implementation review.
+
+Do not change Phase 2 scope, prerequisites, phase-local choices, non-goals, or
+Phase 3 assumptions unless implementation evidence requires an explicit RFC
+correction. Phase 3 should continue to cite the production wrapper,
+operation-entry, error-observation, and deterministic-gate pattern established
+here.
+
+Run paired repeated `doradb-bench run table-ddl` samples on equivalent fresh
+roots and report median and dispersion for successful create/drop cycles.
+One mandatory scheduling hop is expected. Queue delay and execution latency
+must remain visible. The task adds no mandatory work to ordinary transaction
+begin/commit, statements, lookup, insert, or stream paths; any repeatable
+regression there blocks resolution.
+
+### 14. Control the phase-specific risks
+
+The principal correctness risks and required mitigations are:
+
+- **Incomplete prepared authority:** omitting one catalog table or leaving one
+ transaction lock acquisition after acceptance can deadlock/starve mandatory
+ capacity. Keep catalog target lists beside the actual staging/cascade code,
+ assert coverage inside execution, and instrument every lock request in
+ tests.
+- **Nested-state regression:** adding `Mandatory(Some(...))` branches to only
+ the happy path could make rollback, terminal completion, shutdown, or stale
+ cleanup identities incorrect. Keep every `SessionOperationEntry` match
+ exhaustive and add direct transition tests before end-to-end DDL tests.
+- **Panic outside supervision or double panic:** `finish`, `handle_panic`, and
+ nested unwinding Drops run outside or during the sole catch boundary.
+ Remove assertion-based resource Drop, use state-gated non-panicking
+ transitions, and test unwind at statement, transaction-terminal, and
+ catalog/table phase boundaries.
+- **Unsafe residual release:** arbitrary unwind may leave catalog undo, a
+ published root, a closed table lifecycle, or a staged runtime. Prefer
+ `FailedRetained` plus engine poison over speculative compensation; release
+ only logical operation locks and ordinary Rust owners proven safe after
+ retention.
+- **Caller-side lock retention:** complete preparation can hold target/catalog
+ locks while capacity is saturated or a live future stops being polled. This
+ is an accepted RFC consequence; preserve deterministic shutdown diagnostics
+ and document it rather than adding revocation.
+- **Cross-thread test blindness:** thread-local hooks can falsely pass while
+ production work runs elsewhere. Use engine-scoped event-driven hooks and run
+ the focused matrix with one and multiple runners.
+- **Scheduling overhead:** successful DDL gains a queue/cross-thread hop.
+ Measure fresh-root latency and dispersion; do not hide queue time. Keep
+ transaction/statement hot paths unchanged and treat their repeatable
+ regression as a blocker.
+- **Scope expansion into index/maintenance:** shared helpers may reveal later
+ needs, but this phase implements only the narrow table/catalog authority
+ proven by CREATE/DROP. Record broader gates, chunking, or scheduling work for
+ the owning RFC phase or a backlog item.
+
+## Implementation Notes
+
+- `CREATE TABLE` and `DROP TABLE` now prepare their complete fixed logical-lock
+ sets in the caller, transfer one owned DDL scope through mandatory admission,
+ and execute all file, catalog, transaction, and lifecycle effects on the
+ engine-owned mandatory runtime. Prepared catalog statements consume typed
+ operation-lock authority without transaction or statement lock-manager
+ acquisition.
+- Mandatory session operations now support nested private-transaction states
+ and state-gated normal finalization. Accepted execution panic retains unsafe
+ nested state as `FailedRetained`; normal and panic settlement release the
+ operation locks without fallible destructor cleanup. Test-only DDL phases,
+ gates, and failure injection are engine-scoped helpers inside
+ `catalog::table::tests`.
+- Implementation review simplified ownership without changing behavior:
+ CREATE file ownership is one mutually exclusive progress enum; public and
+ private transaction initialization return only the values their callers
+ need; finish readiness is represented directly by the accepted-scope state;
+ and DDL preparation plus mandatory submission/supervision are inherent
+ methods on their owning guards. The mandatory operation guard intentionally
+ retains the exact stable entry so nested transaction state does not require
+ lifecycle relookup.
+- `PreparedCatalogWriteAuthority` remains a deliberate phase-local bridge.
+ Reacquiring the catalog claims for the nested transaction is correctness-safe
+ under current same-family coverage but would duplicate manager grants and
+ owner-cache entries and weaken the prepared no-reacquisition boundary.
+ Backlog `000171` owns unifying operation and transaction claims in the
+ exact-family lock redesign and removing this special path afterward.
+- Release measurements on 2026-08-01 used one thread/session, `log-sync=none`,
+ and equivalent fresh roots. Seven one-cycle `table-ddl` samples had a
+ candidate median of 585,711 ns per create/drop cycle (range
+ 372,919-1,537,467 ns) versus 638,670 ns on `origin/main` (range
+ 353,835-1,097,798 ns). The distributions overlap substantially and show no
+ repeatable regression from the mandatory scheduling hop.
+- Five 500,000-operation hot-path samples showed candidate medians of
+ 296.962 ns/op for `trx-noop` and 73.433 ns/op for `stmt-noop`, versus
+ 308.449 ns/op and 74.357 ns/op respectively on `origin/main`. The benchmark
+ reports caller-visible aggregate latency, so the mandatory queue and
+ execution contribution remains included rather than split into synthetic
+ sub-measurements.
+- Final verification passed 1,621 workspace tests, 1,528 alternate-`libaio`
+ tests, focused preparation/acceptance/panic/cleanup coverage, formatting and
+ diff checks, and the mandatory style audit over 11 branch-diff Rust files.
+
+## Impacts
+
+### Primary implementation
+
+- `doradb-storage/src/session.rs`
+ - `Session::{create_table,drop_table}`
+ - new caller preparation helpers
+ - `SessionOperationPin`
+ - `MandatoryOperationGuard`
+ - prepared/accepted table-DDL scopes
+ - session-operation registry tests and test hook access
+- `doradb-storage/src/catalog/table.rs`
+ - remove `create_table_for_session` and `drop_table_for_session`
+ - `CreateTableProgress`
+ - remove `DropTableProgressGuard`
+ - add `DropTableProgress`
+ - create/drop plans
+ - `PreparedCreateTable` / `AcceptedCreateTable`
+ - `PreparedDropTable` / `AcceptedDropTable`
+ - catalog target lists, staging/cascade, compensation, and failure hooks
+- `doradb-storage/src/catalog/mod.rs`
+ - current-live runtime access used by DROP preparation
+ - CREATE runtime-install invariant tests
+- `doradb-storage/src/trx/mod.rs`
+ - mandatory nested private-transaction entry transitions
+ - mandatory finish validation and non-panicking publication
+ - terminal/rollback/cleanup matches and tests
+- `doradb-storage/src/trx/stmt.rs`
+ - `PreparedCatalogWriteAuthority`
+ - prepared catalog statement staging
+ - catalog insert/delete lock bypass under typed authority
+- `doradb-storage/src/lock/state.rs`
+ - reuse `OwnerLockState` as exact operation grant record and authority proof;
+ add only narrow read/access support if required
+- `doradb-storage/src/lock/mod.rs`
+ - acquisition instrumentation/tests; no manager redesign
+- `doradb-storage/src/error.rs`
+ - narrow native carrier-to-completion conversion used by accepted DDL
+- `doradb-storage/src/runtime/mandatory.rs`
+ - production use of Phase 1 prepared/accepted APIs
+ - remove obsolete production `dead_code` expectations
+ - no scheduler or supervision topology redesign
+
+### Documentation and validation
+
+- `docs/engine-component-lifetime.md`
+- `docs/transaction-system.md`
+- `docs/lock-system.md`
+- `docs/table-file.md`
+- `docs/benchmark-tool.md`
+- `docs/rfcs/0026-engine-owned-mandatory-background-runtime.md` at resolve
+- `doradb-bench` existing `table-ddl` workload and lifecycle tests; production
+ benchmark code changes only if needed to expose already-required queue versus
+ execution measurements
+
+### Public behavior
+
+- Public method signatures and successful catalog/table semantics do not
+ change.
+- Before acceptance, dropping CREATE/DROP remains cancellation and releases
+ preparation.
+- After acceptance, dropping the public future or observer no longer abandons
+ table DDL; the engine completes, compensates, poisons, or safely retains it.
+- A live but unpolled pre-acceptance future may retain logical locks and keep
+ shutdown busy by documented design.
+- Accepted table DDL consumes mandatory caller capacity from acceptance through
+ normal finish or panic retention/release.
+
+### Performance
+
+- Successful CREATE/DROP adds one mandatory-capacity check and executor hop.
+- Preparation may retain complete operation locks while waiting for capacity.
+- No CREATE duplicate-ID/catalog-row lookup is added.
+- DROP removes the extra catalog-row lookup and uses one synchronous
+ current-live runtime resolution under exclusion.
+- Ordinary public transaction and statement hot paths do not use the mandatory
+ runtime or prepared catalog authority.
+
+## Test Cases
+
+### A. Pure preflight and validation
+
+1. Construct but do not poll `Session::create_table`; assert no operation ID,
+ table ID, waiter/grant, permit, task, file, or catalog effect.
+2. Construct but do not poll `Session::drop_table`; assert the same.
+3. Invalid CREATE columns/index shapes fail before session-operation
+ reservation and table-ID allocation.
+4. User `IndexAttributes::PK` fails before operation reservation, locks, or file
+ creation and preserves task `000206` behavior.
+5. A catalog-range/non-user DROP ID fails before operation reservation and
+ lock acquisition.
+6. CREATE ID allocation remains atomic/monotonic; cancellation after allocation
+ may consume one gap without probing or reusing a current ID.
+7. Instrument CREATE preparation to prove it performs no runtime-map,
+ metadata-history, or `catalog.tables` duplicate-ID lookup.
+8. A missing DROP target returns `TableNotFound` after target exclusion and
+ performs no catalog-row scan.
+9. DROP rejects a same-session explicit target lock without entering a
+ self-conflicting lock wait.
+
+### B. Prepared lock acquisition and caller cancellation
+
+10. Assert CREATE owns exactly 9 grants with the approved resources, modes,
+ operation owner, and canonical order.
+11. Assert DROP owns exactly 12 grants with the approved resources, modes,
+ operation owner, and canonical order.
+12. Block and drop preparation during each target/catalog metadata and data
+ lock wait; assert the current waiter and every earlier grant are released
+ exactly once.
+13. Inject an error after each partial successful grant and assert the same
+ cleanup.
+14. Retain a fully prepared but unpolled future and assert locks remain held,
+ its session entry is `Voluntary`, and `try_shutdown` reports it as busy.
+15. Drop that retained future and assert locks release before outer operation
+ terminal publication and shutdown progress wakes.
+16. Queue a conflicting public transaction behind prepared CREATE/DROP, cancel
+ preparation, and assert the transaction proceeds without a protection gap
+ or leaked waiter.
+17. Run distinct-table prepared DDL concurrently and assert no unintended
+ target-table conflict beyond shared catalog write locks.
+
+### C. Capacity and atomic acceptance
+
+18. Configure one caller permit and hold the first accepted task before its
+ first effect. Fully prepare a second table DDL and assert it owns all locks
+ but has no permit, accepted task, or `Mandatory` state.
+19. Drop the capacity-waiting second future and assert complete preparation
+ release.
+20. Release capacity and prove one non-yielding poll moves the exact
+ `PreparedTableDdlScope` once, transitions
+ `Voluntary(None) -> Mandatory(None)` once, spawns, and detaches.
+21. Allow a runner to poll immediately at acceptance and assert no race can
+ expose idle/terminal state before the accepted owner exists.
+22. Poison or close mandatory admission while capacity is pending; assert the
+ prepared owner releases and no effect begins.
+23. Assert accepted metadata carries the exact operation key, table ID, and
+ stable CREATE/DROP label.
+
+### D. No hidden operation-lock acquisition
+
+24. Instrument every `LockManager` acquisition by owner/resource/time and prove
+ all accepted CREATE requests occurred before acceptance.
+25. Prove the same for DROP.
+26. Fail the test if the nested private transaction attempts catalog metadata
+ `S` or data `IX` after acceptance.
+27. For each actual CREATE catalog table, assert prepared authority covers
+ metadata `S` and data `IX`.
+28. For each actual DROP catalog table, including replay silent watermarks,
+ assert the same.
+29. Remove one synthetic prepared grant before an accepted catalog call and
+ assert the coverage invariant panics inside `execute`, is caught, poisons,
+ and does not kill the runner.
+30. Exercise the ordinary non-prepared catalog statement path and assert it
+ still acquires transaction locks.
+
+### E. Mandatory nested transaction states
+
+31. Cover
+ `Mandatory(None) -> Mandatory(Available) -> Mandatory(Running) ->
+ Mandatory(Available)` for successful statement checkout/return.
+32. Cover terminal claim and successful commit back to `Mandatory(None)`.
+33. Cover explicit rollback back to `Mandatory(None)`.
+34. Cover statement error plus whole-private-transaction rollback.
+35. Assert nested transaction completion never publishes outer `Terminal`.
+36. Assert outer normal terminal is published only after prepared lock release.
+37. Drop a nested transaction during an injected execute panic and assert no
+ competing abandoned-cleanup task is submitted.
+38. Assert a retained nested core remains visible as `FailedRetained` and
+ blocks shutdown.
+39. Preserve unmigrated `Voluntary(Some(...))` index DDL and maintenance state
+ tests.
+
+### F. Observer and public-future drop
+
+40. Drop the public CREATE future/observer immediately after acceptance and
+ before first effect; assert CREATE still reaches a terminal result.
+41. Repeat after file creation, private transaction begin, catalog staging,
+ file publication, runtime build, commit, and before/after installation.
+42. Drop the public DROP future/observer before lifecycle close, while drain is
+ pending, after drain, during catalog cascade, during commit, and before
+ retained-runtime publication.
+43. Race observer drop with result publication and assert the output is
+ consumed or logged exactly once without influencing execution.
+44. Close or abandon the public `Session` after acceptance and assert the
+ registry retains the exact mandatory operation until finalization.
+
+### G. CREATE ordinary failures and compensation
+
+45. File creation failure creates no transaction/catalog/runtime state.
+46. Private transaction begin failure deletes the provisional file.
+47. Failure after catalog staging rolls back catalog effects and deletes the
+ file.
+48. Failure during file/root publication performs existing precommit cleanup.
+49. Failure during runtime build destroys staged state, rolls back, and deletes
+ the file.
+50. Failure after runtime build and before commit follows the same policy.
+51. Commit failure after root publication destroys the staged runtime, poisons,
+ and retains the file according to current recovery policy.
+52. An impossible runtime-map duplicate asserts only inside execute, reaches
+ mandatory panic handling, poisons, and never returns an ordinary ID
+ conflict.
+53. Successful CREATE installs one current-live runtime with matching catalog
+ rows/file metadata and releases all operation/catalog locks.
+
+### H. DROP ordinary failures and irreversible policy
+
+54. Private transaction begin failure leaves the table live.
+55. `start_drop_lifecycle` failure rolls back without poisoning.
+56. Hold foreground/runtime publication work and assert DROP waits in the
+ accepted drain phase while target/catalog locks and permit remain owned.
+57. Catalog cascade failure after lifecycle close preserves the original
+ source, attempts rollback, poisons, and never reopens foreground access.
+58. Inject rollback failure after the gate and assert it is diagnostic cleanup,
+ not a replacement for the original poison source.
+59. Commit failure after the gate poisons and retains the unsafe residual.
+60. Dropped-runtime/replay-floor retention failure poisons.
+61. Successful DROP deletes the five catalog row families, publishes retained
+ runtime/floor state, requests both purge classes, and releases locks only
+ afterward.
+62. Concurrent DROP of the same table waits for the first target lock and then
+ observes `TableNotFound` from the current-live map without a stale Arc.
+
+### I. Panic-only execution and non-panicking settlement
+
+63. Inject an execution panic before CREATE's first effect and after each
+ representative CREATE phase.
+64. Inject an execution panic before DROP's lifecycle gate and after each
+ representative irreversible DROP phase.
+65. Assert each panic calls `handle_panic`, publishes
+ `MandatoryTaskPanic`, poisons, moves the entry to `FailedRetained`, releases
+ the caller permit, and leaves the executor runner alive.
+66. Assert panic hooks cannot fire in preparation, `accept`, `finish`,
+ `handle_panic`, or any Drop implementation.
+67. Exercise both production `PreparedExecution::accept` implementations
+ inside `catch_unwind`; assert the retained-entry handoff does not unwind
+ and moves the exact operation/lock scope once.
+68. Exercise `AcceptedCreateTable::finish` and
+ `AcceptedDropTable::finish` under normal ready states inside
+ `catch_unwind`; assert no unwind and correct release-before-terminal order.
+69. Exercise both `handle_panic` implementations inside `catch_unwind`; assert
+ no unwind and no fallible compensation.
+70. Drop progress and prepared lock scopes for every synthetic phase inside
+ `catch_unwind`; assert no debug assertion, poison-from-progress-Drop, or
+ release panic.
+71. Panic from inside a prepared catalog statement after residual row/index
+ effects exist; assert there is no double panic, partial redo is discarded,
+ undo/core ownership reaches `FailedRetained`, and the outer supervisor
+ completes.
+72. Panic while a nested catalog transaction owns a terminal claim or prepared
+ transaction before its supervised terminal handoff; assert no active core,
+ undo, lock state, or attachment is dropped and no assertion-bearing
+ destructor double-panics.
+73. Exercise the defensive missing-terminal-ready state and assert fatal
+ retention/poison rather than panic or idle publication.
+74. Assert accepted panic releases operation locks only through the
+ non-panicking outer resource scope after safe fatal retention and generic
+ poison publication.
+
+### J. Shutdown, runner count, recovery, and regression
+
+75. With one runner, hold accepted CREATE/DROP at each execution-internal await
+ and assert blocking shutdown drains rather than cancels it.
+76. Repeat focused ownership, panic, and shutdown tests with multiple runners.
+77. Assert `try_shutdown` distinguishes retained `Voluntary` preparation,
+ accepted `Mandatory` table DDL, nonzero caller permits, and
+ `FailedRetained`.
+78. Race shutdown admission close with prepared capacity waiting and with
+ accepted work; only the prepared waiter is rejected/cancelled.
+79. Assert mandatory runtime workers stop only after accepted table DDL and
+ internal cleanup drain and the executor is empty.
+80. Recover after successful CREATE, successful DROP, injected precommit
+ rollback, and post-root/commit poison residue; preserve existing catalog,
+ allocator, file cleanup, replay-floor, and dropped-runtime invariants.
+81. Re-run existing table DDL, catalog checkpoint, dropped-table purge,
+ metadata-history purge, file cleanup, explicit lock, DML admission, and
+ recovery tests.
+82. Run the standard workspace nextest pass:
+
+ ```bash
+ rtk cargo nextest run --workspace
+ ```
+
+83. Run the alternate storage backend because this task changes table-file
+ creation/publication and recovery-observable behavior:
+
+ ```bash
+ rtk cargo nextest run -p doradb-storage --no-default-features --features libaio
+ ```
+
+84. Run normal build/lint/style validation required by repository guidance and
+ `$task-resolve`, including the mandatory style audit for branch-modified
+ Rust files.
+85. Run repeated release-mode `table-ddl` benchmarks on equivalent fresh roots,
+ report median/dispersion and queue versus execution observations, and
+ compare ordinary transaction/statement baselines for unintended hot-path
+ regression.
+
+## Open Questions
+
+No blocking design questions remain.
+
+- `docs/backlogs/000171-exact-family-lock-system-redesign.md` owns the deferred
+ unification of operation and nested-transaction lock claims and eventual
+ removal of the phase-local prepared catalog-write authority. This does not
+ change RFC-0026 Phase 3 prerequisites.
diff --git a/docs/tasks/next-id b/docs/tasks/next-id
index a7eb3ca7..12fe2aa2 100644
--- a/docs/tasks/next-id
+++ b/docs/tasks/next-id
@@ -1 +1 @@
-000249
+000250
diff --git a/docs/transaction-system.md b/docs/transaction-system.md
index 2ba5aa81..844a21cf 100644
--- a/docs/transaction-system.md
+++ b/docs/transaction-system.md
@@ -267,17 +267,30 @@ DDL and maintenance start private transactions through their already-reserved
operation authority. A private transaction allocates a new `TrxID` and boxed
core but inherits the outer operation key, installs that box in the same entry
mutex, and does not replace the active slot. While the outer foreground
-authority remains attached, `ForegroundRunning(Some(InternalTrxState))`
+authority remains attached, `Voluntary(Some(InternalTrxState))`
records the private transaction's available, checked-out, cleanup, or
completion position. Public transactions use the outer operation states
directly and therefore use
-`ForegroundRunning(None)` only while checked out. A private transaction's terminal
-callback clears the child and returns the entry to `ForegroundRunning(None)`;
-only dropping the outer foreground authority can publish the operation terminal
-and return an open session to idle. One outer operation may run sequential
-private transactions, so the entry's optional `TrxID` changes only at
-installation and terminal completion while remaining protected by that same
-mutex.
+`Voluntary(None)` only while checked out.
+
+Accepted table DDL transfers the same entry to `Mandatory(None)` before the
+runtime task is detached. Its nested catalog transaction follows
+`Mandatory(None) -> Mandatory(Some(Available)) ->
+Mandatory(Some(Running)) -> Mandatory(Some(Available))`; commit or rollback
+claims `Mandatory(Some(Completing))` and clears the child back to
+`Mandatory(None)`. That child terminal edge never publishes the outer
+operation terminal. Successful accepted execution first proves the exact empty
+mandatory state, releases its complete prepared lock scope, and consumes that
+proof to publish `Terminal`. A supervised unwind moves any still-owned nested
+state to `FailedRetained`; this remains registry-visible and blocks shutdown
+instead of exposing an idle session or scheduling competing abandoned cleanup.
+
+Unmigrated index DDL and maintenance retain the voluntary private-transaction
+path. Their private terminal callback clears the child and returns the entry to
+`Voluntary(None)`; only dropping the outer foreground authority publishes the
+operation terminal and returns an open session to idle. One outer operation
+may run sequential private transactions, so the entry's optional `TrxID`
+changes only at installation and terminal completion under that same mutex.
After explicit rollback claims terminal ownership and publishes `RollingBack`,
the claimed transaction core, undo buffers, locks, and session cleanup
@@ -381,18 +394,23 @@ guards, table/layout owners, and logical locks, and then sleeps. This lets
same-table DROP acquire metadata X and publish terminal lifecycle state; the
listener carries that change into the next bounded recheck.
-`CREATE TABLE` allocates a distinct id and then holds `TableMetadata(X)` for
-that id while it creates the deterministic table file, stages catalog rows,
-builds the per-id runtime, commits the catalog transaction, and publishes the
-current history/runtime entry. The initial table-file root uses the create
-transaction STS as `root_ts`. Keeping metadata X through current publication
-prevents first touch from observing a partially published table.
-
-`DROP TABLE` prechecks the id-only runtime and catalog row, acquires
-`TableMetadata(X)` followed by `TableData(X)`, and then revalidates the target
-under those table-local locks before crossing the terminal lifecycle gate. A
-drop that waits for an already-admitted checkpoint publisher therefore does
-not delay CREATE or DROP for unrelated table ids. Transaction and statement
+`CREATE TABLE` validates metadata before reservation, allocates a distinct
+gap-tolerant id, and caller-prepares target metadata X plus metadata-S/data-IX
+authority for the four catalog tables it writes. Mandatory acceptance then
+owns those locks while it creates the deterministic table file, runs its nested
+catalog transaction without further manager acquisition, builds the per-id
+runtime, commits, and publishes the current history/runtime entry. The initial
+table-file root uses the create transaction STS as `root_ts`.
+
+`DROP TABLE` rejects non-user ids and same-session explicit target locks before
+waiting, then caller-prepares target metadata/data X plus metadata-S/data-IX
+authority for all five cascade catalog tables. Under target exclusion it
+selects the exact current-live `Arc
` without an extra catalog-row scan.
+Mandatory execution begins the nested transaction, closes and drains the
+terminal lifecycle, performs the catalog cascade, commits, and publishes
+dropped-runtime/replay-floor retention. A drop waiting for an already-admitted
+checkpoint publisher therefore does not delay CREATE or DROP for unrelated
+table ids when runner capacity is available. Transaction and statement
rollback drop their operation-local table caches and transaction bindings
before releasing the logical locks that authorize those runtime owners.
CREATE INDEX and DROP INDEX also take same-table `TableMetadata(X)`. That grant
diff --git a/doradb-storage/src/catalog/table.rs b/doradb-storage/src/catalog/table.rs
index d4fccad4..dc3a91f2 100644
--- a/doradb-storage/src/catalog/table.rs
+++ b/doradb-storage/src/catalog/table.rs
@@ -1,11 +1,14 @@
use crate::buffer::PoolGuards;
use crate::catalog::spec::{ActiveIndexSpec, ColumnAttributes, ColumnSpec, IndexNo, IndexSpec};
-use crate::catalog::{ColumnObject, IndexColumnObject, IndexObject, TableObject, is_user_table};
+use crate::catalog::{
+ ColumnObject, IndexColumnObject, IndexObject, TableObject, catalog_table_id_from_slot,
+ is_user_table,
+};
use crate::engine::EngineRef;
use crate::error::{
- DiscloseError, DiscloseResultExt, FatalError, FatalResult, InternalError, InternalResult,
- IoResult, OperationError, OperationOrRuntimeResult, OperationResult, Result, RuntimeError,
- RuntimeOrFatalError, RuntimeOrFatalResult, RuntimeResult,
+ CompletionErrorBridge, CompletionResult, FatalError, FatalResult, InternalError,
+ InternalResult, IoResult, OperationError, OperationOrRuntimeResult, OperationResult,
+ RuntimeError, RuntimeOrFatalError, RuntimeOrFatalResult, RuntimeResult,
};
use crate::file::table_file::{MutableTableFile, TableFile};
use crate::id::{TableID, TrxID};
@@ -15,107 +18,235 @@ use crate::map::FastHashSet;
use crate::obs;
use crate::row::ops::SelectKey;
use crate::row::{Row, RowRead};
+use crate::runtime::mandatory::{AcceptedExecution, MandatoryTaskMetadata, PreparedExecution};
use crate::serde::{Deser, DeserResult, MinBytesHint, Ser, Serde, min_bytes_hint};
-use crate::session::{SessionDdlContext, SessionOperationPin};
+use crate::session::{AcceptedTableDdlScope, PreparedTableDdlScope};
use crate::table::{Table, TableRedoReplayFloor};
-use crate::trx::Transaction;
+use crate::trx::{PreparedCatalogWriteAuthority, Transaction};
use crate::value::{Val, ValKind, ValType};
use error_stack::{Report, ResultExt};
use semistr::SemiStr;
+use std::any::Any;
use std::mem;
use std::ops::Index;
use std::result::Result as StdResult;
use std::sync::Arc;
#[cfg(test)]
-use tests::{
- CreateTableTestFailure, maybe_fail_create_table,
- maybe_poison_before_create_table_catalog_commit,
-};
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-enum CreateTablePhase {
- Init,
- CatalogStaged,
- FilePublished,
- RuntimeBuilt,
- CatalogCommitted,
- Installed,
- Aborted,
+use tests::{CreateTableTestFailure, TableDdlTestPhase};
+
+const CREATE_TABLE_CATALOG_WRITE_TARGETS: [TableID; 4] = [
+ catalog_table_id_from_slot(0),
+ catalog_table_id_from_slot(1),
+ catalog_table_id_from_slot(2),
+ catalog_table_id_from_slot(3),
+];
+const DROP_TABLE_CATALOG_WRITE_TARGETS: [TableID; 5] = [
+ catalog_table_id_from_slot(0),
+ catalog_table_id_from_slot(1),
+ catalog_table_id_from_slot(2),
+ catalog_table_id_from_slot(3),
+ catalog_table_id_from_slot(4),
+];
+
+/// Purely validated public CREATE TABLE input.
+pub(crate) struct ValidatedCreateTable {
+ table_spec: super::TableSpec,
+ metadata: Arc,
}
-impl CreateTablePhase {
+impl ValidatedCreateTable {
+ /// Validate public metadata before reserving a session operation or table id.
#[inline]
- fn is_terminal(self) -> bool {
- matches!(self, Self::Installed | Self::Aborted)
+ pub(crate) fn try_new(
+ table_spec: super::TableSpec,
+ index_specs: Vec,
+ ) -> OperationResult {
+ reject_user_table_primary_key_indexes(&index_specs, "create_table")?;
+ let metadata = Arc::new(TableMetadata::try_new(
+ table_spec.columns.clone(),
+ index_specs,
+ )?);
+ Ok(Self {
+ table_spec,
+ metadata,
+ })
+ }
+
+ /// Bind validated metadata to one gap-tolerant allocated table id.
+ #[inline]
+ pub(crate) fn into_plan(self, table_id: TableID) -> CreateTablePlan {
+ let table_object = TableObject {
+ table_id,
+ next_index_no: self.metadata.idx.next_index_no(),
+ };
+ let column_objects = self
+ .table_spec
+ .columns
+ .into_iter()
+ .enumerate()
+ .map(|(col_no, col_spec)| ColumnObject {
+ table_id,
+ column_no: col_no as u16,
+ column_name: col_spec.column_name,
+ column_type: col_spec.column_type,
+ column_attributes: col_spec.column_attributes,
+ })
+ .collect();
+ let mut index_objects = Vec::new();
+ let mut index_column_objects = Vec::new();
+ for (index_no, index_spec) in self.metadata.idx.active_indexes() {
+ index_objects.push(IndexObject {
+ table_id,
+ index_no: index_no as u16,
+ index_attributes: index_spec.attributes,
+ });
+ for (index_column_no, key) in index_spec.cols.iter().enumerate() {
+ index_column_objects.push(IndexColumnObject {
+ table_id,
+ index_no: index_no as u16,
+ index_column_no: index_column_no as u16,
+ column_no: key.col_no,
+ index_order: key.order,
+ });
+ }
+ }
+ CreateTablePlan {
+ table_id,
+ metadata: self.metadata,
+ table_object: Some(table_object),
+ column_objects,
+ index_objects,
+ index_column_objects,
+ }
}
}
-struct DropTableProgressGuard {
- engine: EngineRef,
+/// Owned CREATE TABLE execution plan transferred across mandatory acceptance.
+pub(crate) struct CreateTablePlan {
table_id: TableID,
- armed: bool,
+ metadata: Arc,
+ table_object: Option,
+ column_objects: Vec,
+ index_objects: Vec,
+ index_column_objects: Vec,
+}
+
+/// Owned DROP TABLE target selected under complete target exclusion.
+pub(crate) struct DropTablePlan {
+ table_id: TableID,
+ table: Option>,
}
-impl DropTableProgressGuard {
+impl DropTablePlan {
+ /// Retain the exact current-live runtime selected during preparation.
#[inline]
- fn new(engine: EngineRef, table_id: TableID) -> Self {
+ pub(crate) fn new(table_id: TableID, table: Arc