ELIPS transactions are atomic write batches under the single-writer model. Transaction buffers place and erase calls; commit applies them in order, each one WAL-appended before the in-memory mutation. The destructor auto-rolls back if neither commit() nor rollback() ran.
Class shape
namespace elips {
class TransactionVault {
public:
RecordID place(const Vector& vector, Payload payload = {},
std::optional<RecordID> id = std::nullopt);
void erase(const RecordID& id);
private:
friend class Transaction;
TransactionVault(Transaction& txn, std::string vault)
: txn_(&txn), vault_(std::move(vault)) {}
Transaction* txn_;
std::string vault_;
};
class Transaction {
public:
explicit Transaction(ElipsInstance& db);
~Transaction(); // auto-rolls back if !done_
TransactionVault vault(std::string name);
void commit();
void rollback() noexcept;
private:
void enqueue_place(std::string vault, const Vector&, Payload, std::optional<RecordID>);
void enqueue_erase(std::string vault, RecordID);
ElipsInstance* db_;
std::vector<PendingOp> ops_;
bool done_{false};
};
} // namespace elipsPendingOp queue
struct PendingOp {
bool is_erase{false}; // true = erase, false = place
std::string vault; // target vault name
Vector vector; // empty for erase
Payload payload; // empty for erase
std::optional<RecordID> id; // pre-generated or explicit
};Inserts capture vector + payload + pre-generated id; erases capture only the target id. The buffer is std::vector<PendingOp> — operations apply in the order they were enqueued.
Eager validation
void Transaction::enqueue_place(std::string vault, const Vector& vector,
Payload payload, std::optional<RecordID> id) {
if (vector.dimension() != db_->config().dimension()) {
throw DimensionMismatch{"vector dimension does not match database"};
}
if (!all_finite(vector.values())) {
throw InvalidVector{"vector contains NaN or Inf"};
}
ops_.push_back(PendingOp{false, std::move(vault), vector,
std::move(payload), std::move(id)});
}Validating up front means commit() can never fail mid-batch on dimension or finiteness grounds — the only failure modes left are storage IO errors. Without this, atomicity would be impossible to promise: half the batch could end up applied before a bad vector tripped validation.
RecordID pre-generation
RecordID TransactionVault::place(const Vector& vector, Payload payload,
std::optional<RecordID> id) {
const RecordID assigned = id.value_or(RecordID::generate());
txn_->enqueue_place(vault_, vector, std::move(payload), assigned);
return assigned; // returned before commit
}Generating the UUIDv7 at enqueue time lets callers log or cross-reference the id during the transaction, even though the record is not yet visible to readers.
Commit
void Transaction::commit() {
for (auto& op : ops_) {
Vault& vault = db_->vault(op.vault);
if (op.is_erase) {
vault.erase(*op.id); // WAL + index + store
} else {
vault.place(op.vector, op.payload, op.id); // prepare + WAL + index + store
}
}
ops_.clear();
done_ = true;
}Each operation is WAL-appended individually before the in-memory mutation. A crash mid-commit is consistent: WAL replay reapplies every operation that made it to the log; everything past the crash is lost cleanly. Under the single-writer lock no other mutator can interleave.
Rollback & RAII
void rollback() noexcept { ops_.clear(); done_ = true; }
Transaction::~Transaction() {
if (!done_) {
rollback(); // discard buffered ops; none were applied
}
}Rollback is trivial — nothing was applied. The destructor calls it on any non-finalised transaction, so exceptions and early returns never leave a half-built batch lying around.
Python context manager
with db.begin_transaction() as txn:
v = txn.vault("data")
v.place(vector1, {"tag": "a"})
v.place(vector2, {"tag": "b"})
# clean exit → __exit__ calls commit()
# raised exc → __exit__ does NOT commit; destructor rolls backThe binding uses a TransactionHolder that keeps the Python Database reference alive for the transaction's lifetime, so the C++ ElipsInstance outlives the Transaction as required.
Isolation level
Atomic batched writes under a single-writer lock — effectively serializable isolation. All transactions are serialised by the database lock. Reads within a transaction do not see uncommitted writes; seek always reads from the committed state.
- Atomicity — all or none, via rollback. WAL provides durability.
- Consistency — eager validation rejects bad inputs before they enter the buffer.
- Isolation — single writer prevents dirty reads and lost updates.
- Durability — every operation hits the WAL during commit, flushed per
Durability.
Lifecycle
Related: Lock manager · Storage & recovery · C++ SDK.