250 lines
6.3 KiB
V
250 lines
6.3 KiB
V
module orm
|
|
|
|
const tx_callback_closed_err = 'transaction callback must not close the transaction explicitly'
|
|
|
|
enum TxKind {
|
|
root
|
|
savepoint
|
|
}
|
|
|
|
@[heap]
|
|
struct TxRootState {
|
|
mut:
|
|
next_savepoint_id int = 1
|
|
active bool = true
|
|
}
|
|
|
|
@[heap]
|
|
struct TxInner {
|
|
mut:
|
|
conn TransactionalConnection
|
|
kind TxKind
|
|
active bool = true
|
|
savepoint_name string
|
|
root_state &TxRootState = unsafe { nil }
|
|
}
|
|
|
|
// Tx is an ORM transaction handle that can be used anywhere an orm.Connection is accepted.
|
|
pub struct Tx {
|
|
mut:
|
|
inner &TxInner = unsafe { nil }
|
|
}
|
|
|
|
@[heap]
|
|
struct SavepointInner {
|
|
mut:
|
|
conn TransactionalConnection
|
|
owner &TxInner = unsafe { nil }
|
|
name string
|
|
active bool = true
|
|
}
|
|
|
|
// Savepoint is a manual savepoint handle created from an active transaction.
|
|
pub struct Savepoint {
|
|
mut:
|
|
inner &SavepointInner = unsafe { nil }
|
|
}
|
|
|
|
// begin starts a new ORM transaction on the provided connection.
|
|
pub fn begin(mut conn TransactionalConnection) !Tx {
|
|
conn.orm_begin()!
|
|
return Tx{
|
|
inner: &TxInner{
|
|
conn: conn
|
|
kind: .root
|
|
root_state: &TxRootState{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// transaction runs a callback inside a transaction and commits or rolls back automatically.
|
|
pub fn transaction[T](mut conn TransactionalConnection, f fn (mut Tx) !T) !T {
|
|
mut tx := begin(mut conn)!
|
|
result := f(mut tx) or {
|
|
callback_err := err
|
|
if !tx.is_active() {
|
|
return error(tx_callback_closed_err)
|
|
}
|
|
tx.rollback() or {
|
|
return error('transaction callback failed: ${callback_err.msg()}; rollback failed: ${err.msg()}')
|
|
}
|
|
return callback_err
|
|
}
|
|
if !tx.is_active() {
|
|
return error(tx_callback_closed_err)
|
|
}
|
|
tx.commit()!
|
|
return result
|
|
}
|
|
|
|
// commit commits an active transaction.
|
|
pub fn (mut tx Tx) commit() ! {
|
|
tx.ensure_active('commit')!
|
|
match tx.inner.kind {
|
|
.root {
|
|
tx.inner.conn.orm_commit()!
|
|
tx.inner.root_state.active = false
|
|
}
|
|
.savepoint {
|
|
tx.inner.conn.orm_release_savepoint(tx.inner.savepoint_name)!
|
|
}
|
|
}
|
|
|
|
tx.inner.active = false
|
|
}
|
|
|
|
// rollback rolls back an active transaction.
|
|
pub fn (mut tx Tx) rollback() ! {
|
|
tx.ensure_active('rollback')!
|
|
match tx.inner.kind {
|
|
.root {
|
|
tx.inner.conn.orm_rollback()!
|
|
tx.inner.root_state.active = false
|
|
}
|
|
.savepoint {
|
|
tx.inner.conn.orm_rollback_to(tx.inner.savepoint_name)!
|
|
tx.inner.conn.orm_release_savepoint(tx.inner.savepoint_name)!
|
|
}
|
|
}
|
|
|
|
tx.inner.active = false
|
|
}
|
|
|
|
// savepoint creates a manual savepoint inside an active transaction.
|
|
pub fn (mut tx Tx) savepoint() !Savepoint {
|
|
tx.ensure_active('create a savepoint')!
|
|
name := tx.next_savepoint_name()!
|
|
tx.inner.conn.orm_savepoint(name)!
|
|
return Savepoint{
|
|
inner: &SavepointInner{
|
|
conn: tx.inner.conn
|
|
owner: tx.inner
|
|
name: name
|
|
}
|
|
}
|
|
}
|
|
|
|
// transaction runs a nested transaction backed by a savepoint.
|
|
pub fn (mut tx Tx) transaction[T](f fn (mut Tx) !T) !T {
|
|
tx.ensure_active('start a nested transaction')!
|
|
name := tx.next_savepoint_name()!
|
|
tx.inner.conn.orm_savepoint(name)!
|
|
mut nested := Tx{
|
|
inner: &TxInner{
|
|
conn: tx.inner.conn
|
|
kind: .savepoint
|
|
savepoint_name: name
|
|
root_state: tx.inner.root_state
|
|
}
|
|
}
|
|
result := f(mut nested) or {
|
|
callback_err := err
|
|
if !nested.is_active() {
|
|
return error(tx_callback_closed_err)
|
|
}
|
|
nested.rollback() or {
|
|
return error('transaction callback failed: ${callback_err.msg()}; rollback failed: ${err.msg()}')
|
|
}
|
|
return callback_err
|
|
}
|
|
if !nested.is_active() {
|
|
return error(tx_callback_closed_err)
|
|
}
|
|
nested.commit()!
|
|
return result
|
|
}
|
|
|
|
// rollback rolls back to the savepoint and releases it.
|
|
pub fn (mut sp Savepoint) rollback() ! {
|
|
sp.ensure_active('rollback')!
|
|
sp.inner.conn.orm_rollback_to(sp.inner.name)!
|
|
sp.inner.conn.orm_release_savepoint(sp.inner.name)!
|
|
sp.inner.active = false
|
|
}
|
|
|
|
// release releases the savepoint without rolling back.
|
|
pub fn (mut sp Savepoint) release() ! {
|
|
sp.ensure_active('release')!
|
|
sp.inner.conn.orm_release_savepoint(sp.inner.name)!
|
|
sp.inner.active = false
|
|
}
|
|
|
|
fn (tx Tx) is_active() bool {
|
|
return !isnil(tx.inner) && !isnil(tx.inner.root_state) && tx.inner.active
|
|
&& tx.inner.root_state.active
|
|
}
|
|
|
|
fn (tx Tx) ensure_active(action string) ! {
|
|
if isnil(tx.inner) {
|
|
return error('transaction is not initialized')
|
|
}
|
|
if !tx.inner.active {
|
|
return error('transaction is inactive; cannot ${action}')
|
|
}
|
|
}
|
|
|
|
fn (mut tx Tx) next_savepoint_name() !string {
|
|
if isnil(tx.inner) || isnil(tx.inner.root_state) {
|
|
return error('transaction is not initialized')
|
|
}
|
|
name := 'v_orm_sp_${tx.inner.root_state.next_savepoint_id}'
|
|
tx.inner.root_state.next_savepoint_id++
|
|
return name
|
|
}
|
|
|
|
fn (sp Savepoint) ensure_active(action string) ! {
|
|
if isnil(sp.inner) {
|
|
return error('savepoint is not initialized')
|
|
}
|
|
owner_tx := Tx{
|
|
inner: sp.inner.owner
|
|
}
|
|
if !sp.inner.active || isnil(sp.inner.owner) || !owner_tx.is_active() {
|
|
return error('savepoint is inactive; cannot ${action}')
|
|
}
|
|
}
|
|
|
|
// select forwards ORM select queries through the active transaction.
|
|
pub fn (mut tx Tx) select(config SelectConfig, data QueryData, where QueryData) ![][]Primitive {
|
|
tx.ensure_active('use the transaction')!
|
|
return tx.inner.conn.select(config, data, where)
|
|
}
|
|
|
|
// insert forwards ORM insert queries through the active transaction.
|
|
pub fn (mut tx Tx) insert(table Table, data QueryData) ! {
|
|
tx.ensure_active('use the transaction')!
|
|
tx.inner.conn.insert(table, data)!
|
|
}
|
|
|
|
// update forwards ORM update queries through the active transaction.
|
|
pub fn (mut tx Tx) update(table Table, data QueryData, where QueryData) ! {
|
|
tx.ensure_active('use the transaction')!
|
|
tx.inner.conn.update(table, data, where)!
|
|
}
|
|
|
|
// delete forwards ORM delete queries through the active transaction.
|
|
pub fn (mut tx Tx) delete(table Table, where QueryData) ! {
|
|
tx.ensure_active('use the transaction')!
|
|
tx.inner.conn.delete(table, where)!
|
|
}
|
|
|
|
// create forwards ORM create queries through the active transaction.
|
|
pub fn (mut tx Tx) create(table Table, fields []TableField) ! {
|
|
tx.ensure_active('use the transaction')!
|
|
tx.inner.conn.create(table, fields)!
|
|
}
|
|
|
|
// drop forwards ORM drop queries through the active transaction.
|
|
pub fn (mut tx Tx) drop(table Table) ! {
|
|
tx.ensure_active('use the transaction')!
|
|
tx.inner.conn.drop(table)!
|
|
}
|
|
|
|
// last_id forwards the last inserted id through the wrapped connection.
|
|
pub fn (mut tx Tx) last_id() int {
|
|
if isnil(tx.inner) {
|
|
return 0
|
|
}
|
|
return tx.inner.conn.last_id()
|
|
}
|