///|
/// Require an active transaction before inspecting or mutating its candidate.
fn WriteTransaction::require_active(
  self : WriteTransaction,
) -> Unit raise DatabaseError {
  match self.status.val {
    Active => ()
    Failed => raise DatabaseError::TransactionFailed
    Committing => raise DatabaseError::TransactionCommitInProgress
    Committed => raise DatabaseError::TransactionAlreadyCommitted
    RolledBack => raise DatabaseError::TransactionAlreadyRolledBack
  }
}

///|
/// Persist and publish the detached candidate as one atomic transaction.
pub async fn WriteTransaction::commit(
  self : WriteTransaction,
) -> Unit raise DatabaseError {
  self.require_active()
  let state = self.database.val
  if state.closed {
    raise DatabaseError::DatabaseClosed
  }
  if state.recovery_required {
    raise DatabaseError::DatabaseRecoveryRequired
  }
  if state.transaction_id == 18446744073709551615UL {
    raise DatabaseError::TransactionIdExhausted
  }
  let transaction_id = state.transaction_id + 1UL
  let record = WalRecord::{
    transaction_id,
    operations: wal_encode_transaction_operations(self.operations),
  }
  self.status.update(fn(_) { Committing })
  errdefer (if self.status.val is Committing {
    state.recovery_required = true
    state.active = None
    self.status.update(fn(_) { Failed })
  })
  match state.wal_path {
    Some(path) =>
      wal_append_record(path, record) catch {
        error =>
          match error {
            DatabaseError::IoError(operation="open", ..) => {
              self.status.update(fn(_) { Active })
              raise error
            }
            DatabaseError::CorruptLogRecord(..) => {
              self.status.update(fn(_) { Active })
              raise error
            }
            _ => {
              state.recovery_required = true
              state.active = None
              self.status.update(fn(_) { Failed })
              raise DatabaseError::CommitOutcomeUnknown
            }
          }
      }
    None => ()
  }
  let candidate = self.candidate
  state.committed = candidate
  state.transaction_id = transaction_id
  state.active = None
  self.status.update(fn(_) { Committed })
}

///|
/// Discard the detached candidate and release the database writer slot.
/// Failed transactions may only be recovered through rollback.
pub fn WriteTransaction::rollback(
  self : WriteTransaction,
) -> Unit raise DatabaseError {
  match self.status.val {
    Active | Failed => ()
    Committing => raise DatabaseError::TransactionCommitInProgress
    Committed => raise DatabaseError::TransactionAlreadyCommitted
    RolledBack => raise DatabaseError::TransactionAlreadyRolledBack
  }
  self.database.update(fn(state) {
    state.active = None
    state
  })
  self.status.update(fn(_) { RolledBack })
}

///|
/// Create a node in the detached candidate graph.
pub fn WriteTransaction::create_node(
  self : WriteTransaction,
  labels : Array[String],
  properties : Properties,
) -> NodeId raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  let id = self.candidate.create_node(labels, properties)
  let node = self.candidate.get_node(id)
  self.operations.push(TransactionOperation::CreateNode(id, node))
  id
}

///|
/// Read a detached node from the candidate graph.
pub fn WriteTransaction::get_node(
  self : WriteTransaction,
  id : NodeId,
) -> Node raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.get_node(id)
}

///|
/// Return candidate node IDs carrying a label.
pub fn WriteTransaction::node_ids_with_label(
  self : WriteTransaction,
  label : String,
) -> Array[NodeId] raise DatabaseError {
  self.require_active()
  self.candidate.node_ids_with_label(label)
}

///|
/// Add a label to a candidate node.
pub fn WriteTransaction::add_label(
  self : WriteTransaction,
  id : NodeId,
  label : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.add_label(id, label)
  self.operations.push(TransactionOperation::AddNodeLabel(id, label))
}

///|
/// Remove a label from a candidate node.
pub fn WriteTransaction::remove_label(
  self : WriteTransaction,
  id : NodeId,
  label : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.remove_label(id, label)
  self.operations.push(TransactionOperation::RemoveNodeLabel(id, label))
}

///|
/// Set or replace a candidate node property.
pub fn WriteTransaction::set_node_property(
  self : WriteTransaction,
  id : NodeId,
  key : String,
  value : PropertyValue,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.set_node_property(id, key, value)
  let stored_value = normalize_property_value(key, value)
  self.operations.push(
    TransactionOperation::SetNodeProperty(id, key, stored_value),
  )
}

///|
/// Remove a candidate node property.
pub fn WriteTransaction::remove_node_property(
  self : WriteTransaction,
  id : NodeId,
  key : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.remove_node_property(id, key)
  self.operations.push(TransactionOperation::RemoveNodeProperty(id, key))
}

///|
/// Create a directed edge in the detached candidate graph.
pub fn WriteTransaction::create_edge(
  self : WriteTransaction,
  from : NodeId,
  to : NodeId,
  edge_type : String,
  properties : Properties,
) -> EdgeId raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  let id = self.candidate.create_edge(from, to, edge_type, properties)
  let edge = self.candidate.get_edge(id)
  self.operations.push(TransactionOperation::CreateEdge(edge))
  id
}

///|
/// Read a detached edge from the candidate graph.
pub fn WriteTransaction::get_edge(
  self : WriteTransaction,
  id : EdgeId,
) -> Edge raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.get_edge(id)
}

///|
/// Return detached candidate edges leaving a node.
pub fn WriteTransaction::outgoing_edges(
  self : WriteTransaction,
  node_id : NodeId,
) -> Array[Edge] raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.outgoing_edges(node_id)
}

///|
/// Return detached candidate edges entering a node.
pub fn WriteTransaction::incoming_edges(
  self : WriteTransaction,
  node_id : NodeId,
) -> Array[Edge] raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.incoming_edges(node_id)
}

///|
/// Return candidate edge IDs carrying an edge type.
pub fn WriteTransaction::edge_ids_with_type(
  self : WriteTransaction,
  edge_type : String,
) -> Array[EdgeId] raise DatabaseError {
  self.require_active()
  self.candidate.edge_ids_with_type(edge_type)
}

///|
/// Set or replace a candidate edge property.
pub fn WriteTransaction::set_edge_property(
  self : WriteTransaction,
  id : EdgeId,
  key : String,
  value : PropertyValue,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.set_edge_property(id, key, value)
  let stored_value = normalize_property_value(key, value)
  self.operations.push(
    TransactionOperation::SetEdgeProperty(id, key, stored_value),
  )
}

///|
/// Remove a candidate edge property.
pub fn WriteTransaction::remove_edge_property(
  self : WriteTransaction,
  id : EdgeId,
  key : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.remove_edge_property(id, key)
  self.operations.push(TransactionOperation::RemoveEdgeProperty(id, key))
}

///|
/// Delete a candidate edge.
pub fn WriteTransaction::delete_edge(
  self : WriteTransaction,
  id : EdgeId,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.delete_edge(id)
  self.operations.push(TransactionOperation::DeleteEdge(id))
}

///|
/// Delete a candidate node only when it has no incident edges.
pub fn WriteTransaction::delete_node(
  self : WriteTransaction,
  id : NodeId,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.delete_node(id)
  self.operations.push(TransactionOperation::DeleteNode(id))
}

///|
/// Delete a candidate node and its incident edges.
pub fn WriteTransaction::delete_node_cascade(
  self : WriteTransaction,
  id : NodeId,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.delete_node_cascade(id)
  self.operations.push(TransactionOperation::DeleteNodeCascade(id))
}

///|
/// Define a candidate node-property equality index.
pub fn WriteTransaction::create_node_property_index(
  self : WriteTransaction,
  label : String,
  property : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.create_node_property_index(label, property)
  self.operations.push(
    TransactionOperation::CreateNodePropertyIndex(label, property),
  )
}

///|
/// Return IDs from a candidate node-property equality index.
pub fn WriteTransaction::node_ids_by_property_index(
  self : WriteTransaction,
  label : String,
  property : String,
  value : PropertyValue,
) -> Array[NodeId]? raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.node_ids_by_property_index(label, property, value)
}

///|
/// Define a candidate edge-property equality index.
pub fn WriteTransaction::create_edge_property_index(
  self : WriteTransaction,
  edge_type : String,
  property : String,
) -> Unit raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.create_edge_property_index(edge_type, property)
  self.operations.push(
    TransactionOperation::CreateEdgePropertyIndex(edge_type, property),
  )
}

///|
/// Return IDs from a candidate edge-property equality index.
pub fn WriteTransaction::edge_ids_by_property_index(
  self : WriteTransaction,
  edge_type : String,
  property : String,
  value : PropertyValue,
) -> Array[EdgeId]? raise DatabaseError {
  self.require_active()
  errdefer self.status.update(fn(_) { Failed })
  self.candidate.edge_ids_by_property_index(edge_type, property, value)
}