Skip to content

Begin / Commit / Rollback

By default an ODBC connection is in autocommit mode: every statement is its own transaction, committed immediately. Safe-ish for ad-hoc queries, wrong for anything involving multiple related writes where the whole set must succeed or roll back together.

Connection.begin() turns autocommit off. Until commit() or rollback(), every statement is part of one transaction.

The three methods

fun ref begin():    (TxBegun      | TxBeginError)
fun ref commit():   (TxCommitted  | TxCommitError)
fun ref rollback(): (TxRolledBack | TxRollbackError)

Each has a success primitive and an error branch. Matching on the primitive makes intent visible — | TxCommitted => is hard to misread.

After commit() or rollback() the connection returns to autocommit. Call begin() again for the next transaction.

A happy path, and a rollback

The sample commits two rows, then starts a second transaction that hits a constraint violation and rolls back cleanly.

use "lib:odbc"
use "odbc"

actor Main
  new create(env: Env) =>
    let dsn_name =
      try env.args(1)?
      else "psqlred"
      end

    match Odbc.connect(Dsn("DSN=" + dsn_name))
    | let conn: Connection =>
      conn.exec("DROP TABLE IF EXISTS tut_tx")
      match \exhaustive\ conn.exec(
        "CREATE TABLE tut_tx "
          + "(id INTEGER PRIMARY KEY, label VARCHAR(32))")
      | let _: (USize | NoRowCount) => None
      | let e: ExecError =>
        env.err.print("create: " + e.string()); conn.close(); return
      end

      // Happy path: begin, exec, commit.
      match \exhaustive\ conn.begin()
      | TxBegun => None
      | let e: TxBeginError =>
        env.err.print("begin: " + e.string()); conn.close(); return
      end

      conn.exec("INSERT INTO tut_tx VALUES (1, 'alpha')")
      conn.exec("INSERT INTO tut_tx VALUES (2, 'bravo')")

      match \exhaustive\ conn.commit()
      | TxCommitted => env.out.print("committed 2 rows")
      | let e: TxCommitError =>
        env.err.print("commit: " + e.string())
      end

      // Rollback path: a constraint violation should leave no trace.
      match \exhaustive\ conn.begin()
      | TxBegun => None
      | let e: TxBeginError =>
        env.err.print("begin2: " + e.string()); conn.close(); return
      end

      conn.exec("INSERT INTO tut_tx VALUES (3, 'charlie')")
      match \exhaustive\ conn.exec(
        "INSERT INTO tut_tx VALUES (1, 'duplicate')")
      | let _: (USize | NoRowCount) => None
      | let e: ExecError =>
        env.err.print("insert failed: " + e.string())
        match \exhaustive\ conn.rollback()
        | TxRolledBack => env.out.print("rolled back cleanly")
        | let r: TxRollbackError =>
          env.err.print("rollback: " + r.string())
        end
      end

      // Confirm: table has only the two committed rows.
      match \exhaustive\ conn.query(
        "SELECT COUNT(*) FROM tut_tx")
      | let cursor: Cursor =>
        match cursor.fetch()
        | let row: Row =>
          try
            match \exhaustive\ row.int(ColIndex(1))?
            | let n: I64 =>
              env.out.print("rows committed: " + n.string())
            | SqlNull => None
            end
          end
        end
        cursor.close()
      | let e: ExecError =>
        env.err.print("count: " + e.string())
      end

      conn.exec("DROP TABLE IF EXISTS tut_tx")
      conn.close()

    | let e: ConnectError =>
      env.err.print("connect: " + e.string())
    end
./build/07-transactions
committed 2 rows
insert failed: ExecError: constraint violation [23505]
rolled back cleanly
rows committed: 2

23505 is Postgres’s “unique constraint violation”. The library classifies that into the ConstraintViolation kind automatically.

Rollback-on-error is the idiom

conn.begin()
conn.exec(first_statement)
match conn.exec(second_statement)
| let e: ExecError =>
  conn.rollback()  // server may have already rolled back
  return
end
conn.commit()

Once any statement in a transaction errors, assume the transaction is dead and roll back explicitly. Most drivers put the session into an “aborted” state where every subsequent statement errors until rollback — the library surfaces this rather than silently clearing state.

Close auto-rolls-back

Connection.close() fires a rollback before freeing handles. You don’t have to remember rollback in cleanup — but an explicit rollback when you know you’re aborting is still good practice.

What’s next

The three error unions carry more detail than “it failed” — especially TxCommitError, which distinguishes “the server rolled it back” from “we don’t know what happened”. Transaction Errors covers that.