Transactions
Velocity provides full transaction support across all supported databases, allowing you to group multiple operations into an atomic unit of work that either succeeds completely or is rolled back entirely.
Overview
A transaction guarantees the ACID properties for the operations it contains:
- Atomicity — All operations succeed, or none of them are applied
- Consistency — The database moves from one valid state to another
- Isolation — Concurrent transactions do not see each other's uncommitted changes (to a degree determined by the isolation level)
- Durability — Once committed, changes survive system failure
Transactions are managed through Manager using three methods:
- BeginTransaction — Opens a transaction on the current connection
- CommitTransaction — Commits all changes made since
BeginTransaction - RollbackTransaction — Discards all changes made since
BeginTransaction
Basic Usage
The standard pattern wraps DML operations in a try/catch block, committing on success and rolling back on failure:
using (var manager = new Manager(connection))
{
manager.BeginTransaction();
try
{
Record order = new Record(ordersTable);
order.SetFieldInteger("customer_id", 42);
order.SetFieldDecimal("total", 149.99M);
long orderId = manager.AddRecord(order);
Record line = new Record(orderLinesTable);
line.SetFieldLong("order_id", orderId);
line.SetFieldInteger("product_id", 7);
line.SetFieldInteger("quantity", 3);
manager.AddRecordNoIdentity(line);
manager.CommitTransaction();
}
catch
{
manager.RollbackTransaction();
throw;
}
}
Isolation Levels
Velocity supports standard ADO.NET isolation levels via an overload of BeginTransaction:
manager.BeginTransaction(IsolationLevel.Serializable);
The available levels are defined by System.Data.IsolationLevel:
| Isolation Level | Description |
|---|---|
ReadCommitted |
Only committed data is visible; the default for most databases |
ReadUncommitted |
Allows dirty reads — a transaction can see uncommitted changes from other transactions |
RepeatableRead |
Rows read once cannot be changed by other transactions before the current transaction ends |
Serializable |
Full isolation; transactions are executed as if they were serial |
Per-Database Support
Not all databases support every isolation level. Attempting to use an unsupported level will throw an exception from the underlying ADO.NET provider.
| Database | ReadUncommitted | ReadCommitted | RepeatableRead | Serializable |
|---|---|---|---|---|
| DB2 | ✓ | ✓ | ✓ | ✓ |
| MySQL | ✓ | ✓ | ✓ | ✓ |
| Oracle | — | ✓ | — | ✓ |
| PostgreSQL | ✓ ¹ | ✓ | ✓ ² | ✓ |
| SQL Server | ✓ | ✓ | ✓ | ✓ |
| SQLite | — | ✓ ³ | — | ✓ |
| Teradata | ✓ | ✓ | ✓ | ✓ |
¹ PostgreSQL accepts ReadUncommitted but treats it as ReadCommitted — dirty reads never occur.
² PostgreSQL treats RepeatableRead as Serializable internally.
³ SQLite operates in serialized mode by default; ReadUncommitted requires WAL journal mode.
DDL and Transactions
Database systems differ fundamentally in how they handle DDL statements (CREATE TABLE, CREATE INDEX, DROP TABLE, etc.) inside an active transaction. Velocity accounts for this automatically.
Transactional DDL Databases
SQL Server, PostgreSQL, and SQLite support DDL inside transactions. This means schema changes can be rolled back just like data changes:
using (var manager = new Manager(connection))
{
Schema schema = manager.LoadSchema("myschema");
manager.BeginTransaction();
try
{
// Create a new table inside the transaction
Table staging = schema.CreateTable("staging_import");
staging.CreateColumn("id", DataType.Integer, true, true);
staging.CreateColumn("payload", DataType.VarChar, size: 500);
manager.CreateTable(staging);
// Insert data into the staging table
Record r = new Record(staging);
r.SetFieldVarChar("payload", "data");
manager.AddRecord(r);
manager.CommitTransaction();
// Both the table creation and the insert are now permanent
}
catch
{
manager.RollbackTransaction();
// Both the table creation and the insert are rolled back
throw;
}
}
Auto-Commit DDL Databases
MySQL, Oracle, DB2, and Teradata issue an implicit commit whenever a DDL statement is executed. This is a fundamental behaviour of the database engine itself — it cannot be suppressed at the driver level.
Warning
On MySQL, Oracle, DB2, and Teradata, executing any DDL statement (CREATE TABLE, CREATE INDEX, DROP TABLE, ALTER TABLE, etc.) will automatically commit any pending DML in the current transaction. Do not mix DDL and DML within the same transaction on these databases.
Velocity handles this safely by detecting the database type at runtime. DDL commands on auto-commit databases are always executed outside the active transaction, so Velocity's transaction object remains consistent. However, any DML that was pending before the DDL executes will have been implicitly committed by the database engine.
// ✗ Avoid on MySQL, Oracle, DB2, Teradata
using (var manager = new Manager(connection))
{
manager.BeginTransaction();
manager.AddRecord(importantRecord); // pending in transaction
manager.CreateIndex(newIndex); // implicit commit fires here —
// importantRecord is now committed
// regardless of what follows
manager.RollbackTransaction(); // nothing left to roll back
}
// ✓ Correct pattern: DDL first, then DML transaction
using (var manager = new Manager(connection))
{
manager.CreateIndex(newIndex); // DDL outside any transaction
manager.BeginTransaction();
manager.AddRecord(importantRecord); // safely in transaction
manager.CommitTransaction();
}
Full-Text Index Commands
Full-text index operations on SQL Server are a special case. Even though SQL Server supports transactional DDL in general, the following operations cannot participate in a transaction at all and will fail if one is active:
CREATE FULLTEXT CATALOGCREATE FULLTEXT INDEXDROP FULLTEXT INDEXDROP FULLTEXT CATALOGALTER FULLTEXT INDEX(used by UpdateFullTextIndex)
Velocity handles this automatically — full-text index commands on SQL Server always execute on a separate connection that carries no transaction, regardless of whether BeginTransaction has been called. Any DML operations in the active transaction are unaffected and can still be committed or rolled back normally.
Warning
CREATE FULLTEXT INDEX requires a schema modification lock (SCH-M) on the target table. If the active transaction already holds row locks on that table (from a prior INSERT, UPDATE, or SELECT), the full-text operation will block waiting for those locks to be released and will eventually time out. Always perform full-text DDL before acquiring row locks on the same table.
using (var manager = new Manager(connection))
{
// Schema and full-text index are built outside any transaction.
// This must happen before any DML transaction touches the same table.
manager.BuildSchema(schema, overwrite: true);
manager.CreateFullTextIndex(table.FullTextIndex);
manager.BeginTransaction();
// DML is enrolled in the transaction as normal
manager.AddRecord(articleRecord);
manager.CommitTransaction(); // commits only the DML
}
Summary
The table below shows which DDL commands participate in an active transaction on each database:
| Database | DdlTransactions | Regular DDL in Tx | Full-Text DDL in Tx |
|---|---|---|---|
| DB2 | — | — | — |
| MySQL | — | — | — |
| Oracle | — | — | — |
| PostgreSQL | ✓ | ✓ | ✓ |
| SQL Server | ✓ | ✓ | — ¹ |
| SQLite | ✓ | ✓ | ✓ |
| Teradata | — | — | — |
¹ SQL Server full-text operations (CreateFullTextIndex, DropFullTextIndex, UpdateFullTextIndex) always execute outside any active transaction.
Multiple Operations Example
The following example shows a typical pattern for transferring funds between accounts atomically. Velocity does not support in-place arithmetic in UPDATE statements, so the current balance is read first and the new value is calculated in C# before being written back — all within a single serializable transaction:
using (var manager = new Manager(connection))
{
Table accounts = schema["accounts"];
manager.BeginTransaction(IsolationLevel.Serializable);
try
{
// Read and debit the source account
decimal sourceBalance;
using (ResultSet result = manager.Retrieve(
new Query()
.Select([accounts["balance"]])
.From(accounts)
.Where(new Criterion<int>(accounts["account_id"],
ConditionalType.Equals, sourceAccountId))))
{
sourceBalance = result[0].GetFieldDecimal("balance");
}
Update debit = new Update(accounts,
new Criterion<int>(accounts["account_id"],
ConditionalType.Equals, sourceAccountId));
debit.SetFieldDecimal("balance", sourceBalance - 500M);
manager.UpdateRecords(debit);
// Read and credit the destination account
decimal destBalance;
using (ResultSet result = manager.Retrieve(
new Query()
.Select([accounts["balance"]])
.From(accounts)
.Where(new Criterion<int>(accounts["account_id"],
ConditionalType.Equals, destAccountId))))
{
destBalance = result[0].GetFieldDecimal("balance");
}
Update credit = new Update(accounts,
new Criterion<int>(accounts["account_id"],
ConditionalType.Equals, destAccountId));
credit.SetFieldDecimal("balance", destBalance + 500M);
manager.UpdateRecords(credit);
manager.CommitTransaction();
}
catch
{
manager.RollbackTransaction();
throw;
}
}
Best Practices
Always pair
BeginTransactionwith aCommitTransactionorRollbackTransaction— An uncommitted transaction holds locks and consumes resources until the connection closes.Use
try/catch/throw— Wrap transactional blocks so that any exception causes a rollback and is still propagated to the caller.Keep transactions short — Long-running transactions hold locks that block other connections. Do as little work as possible between
BeginTransactionandCommitTransaction.Do not mix DDL and DML on auto-commit databases — On MySQL, Oracle, DB2, and Teradata, DDL implicitly commits the transaction. Perform all schema changes before opening a data transaction.
Use the appropriate isolation level — Higher isolation levels improve correctness but reduce concurrency. Use
ReadCommitted(the default) unless you have a specific reason to escalate.Schema operations rarely need transactions — DDL is typically performed during schema setup, not during normal application operation. Reserve transactions for DML operations.
See Also
- Data Manipulation Language (DML) - INSERT, UPDATE, and DELETE operations
- Schema Management - Creating and modifying database schemas
- Indexing - Creating and managing indexes
- Datasource Limitations - Per-database feature restrictions