Skip to content

Finish RAII transaction migration in MCP Engine #72

@StefanSteiner

Description

@StefanSteiner

Summary

Finish the transaction-API migration by moving hyperdb-mcp::Engine::execute_in_transaction (and its ~7 closure call sites) off the deprecated raw Connection::begin_transaction / commit / rollback methods and onto the RAII Transaction guard. This is the remaining hold-out from #69 (PR #71).

Background

#69 retired the &self raw transaction trio from hyperdb-api's public surface (deprecated, hidden from rustdoc) and migrated MCP's four async pool-path ingest functions to the RAII guard. The sync path through Engine::execute_in_transaction was deliberately not migrated because it's a structural cascade rather than a mechanical one.

Engine today holds the connection by value and exposes operations as &self methods so its containing Arc<RwLock<Engine>> lock model can support concurrent read-only reuse. The RAII guard requires &mut self on Connection::transaction, so any path that closes over Engine while holding a transaction can't use the guard without one of two structural changes.

The deprecated call sites

hyperdb-mcp/src/engine.rs:725-770Engine::execute_in_transaction(&self, f: F) annotated with #[allow(deprecated, reason = "Engine borrows &self; the RAII guard requires &mut. Migration tracked as a follow-up.")]. Body uses:

  • self.connection.begin_transaction() — line 727
  • self.connection.commit() — line 744
  • self.connection.rollback() — line 749 (error path)
  • self.connection.rollback() — line 769 (panic recovery path)

8 closure callers across the workspace, each passing |engine| { ... engine.execute_command(...) ... }:

  • hyperdb-mcp/src/ingest.rs:652, 789, 883
  • hyperdb-mcp/src/ingest_arrow.rs:318, 611
  • hyperdb-mcp/src/lakehouse.rs:196
  • hyperdb-mcp/src/server.rs:2270
  • Test/integration sites in hyperdb-mcp/tests/

Two implementation paths

Path A — Engine::transaction(&mut self) -> Result<EngineTransaction<'_>>

Add an analogue of hyperdb-api::Transaction at the Engine level. The guard delegates the same query/insert/catalog helpers &Engine exposes today, but its existence borrows &mut Engine exclusively (matching Transaction's &mut Connection borrow).

Closure call sites rewrite:

// before
engine.execute_in_transaction(|engine| {
    engine.create_table_in(...)?;
    engine.execute_command(&sql)?;
    Ok(row_count)
})?;

// after
let txn = engine.transaction()?;
txn.create_table_in(...)?;
txn.execute_command(&sql)?;
let row_count = ...;
txn.commit()?;

Pro: mirrors the Connection / Transaction pattern symmetrically; compile-time exclusivity at the engine boundary.
Con: requires every closure callsite to be flattened (no more closure scope). EngineTransaction needs to expose the subset of Engine methods that callers actually use inside transactions — non-trivial surface to design.

Path B — Mutex<Connection> inside Engine

Keep Engine's &self external API, but wrap connection so a &self method can transiently take &mut. execute_in_transaction becomes:

pub fn execute_in_transaction<F, T>(&self, f: F) -> Result<T, McpError>
where F: FnOnce(&Engine) -> Result<T, McpError>
{
    let mut conn = self.connection.lock()?;  // or tokio::Mutex for async paths
    let txn = conn.transaction()?;
    // ... work ...
}

Pro: preserves all existing closure call sites unchanged.
Con: introduces runtime locking where there was none. Every &self method that accesses the connection now competes for the same mutex. Possibly fine (the connection was already serialized by Connection's wire protocol), but it's a real model change. Also, the panic-safety story (std::panic::catch_unwind already in execute_in_transaction) needs to be re-validated under lock-poisoning semantics.

Recommendation

Path A is the conceptually cleaner end state. Path B is a smaller diff. Pick after measuring the surface area EngineTransaction would need to expose — if more than ~6 methods, Path B is probably the pragmatic call.

Acceptance criteria

  • Engine::execute_in_transaction no longer calls the deprecated Connection::begin_transaction / commit / rollback. The #[allow(deprecated, reason = "...")] annotation can be removed.
  • All 8 closure call sites still work (or are rewritten without behavior change).
  • Panic safety preserved — the std::panic::catch_unwind + best-effort rollback story at engine.rs:740-771 continues to work.
  • No new Mutex-related deadlock risks; if Path B is chosen, document the lock ordering with respect to HyperMcpServer's outer Arc<RwLock<Engine>> (or whatever wraps Engine).
  • After this lands, the deprecated pub fn begin_transaction / commit / rollback on Connection and AsyncConnection have zero in-tree callers (verify with grep). At that point a follow-up PR can delete the deprecated wrappers and the _raw rename, completing the API consolidation.

Related

Estimate

Path A: ~1 day (designing EngineTransaction API surface + rewriting 8 call sites + tests).
Path B: ~2 hours (wrap connection in Mutex, update execute_in_transaction, re-verify lock interaction).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions