/* KInterbasDB Python Package - Implementation of Transaction class
 *
 * Version 3.3
 *
 * The following contributors hold Copyright (C) over their respective
 * portions of code (see license.txt for details):
 *
 * [Original Author (maintained through version 2.0-0.3.1):]
 *   1998-2001 [alex]  Alexander Kuznetsov   <alexan@users.sourceforge.net>
 * [Maintainers (after version 2.0-0.3.1):]
 *   2001-2002 [maz]   Marek Isalski         <kinterbasdb@maz.nu>
 *   2002-2007 [dsr]   David Rushby          <woodsplitter@rocketmail.com>
 * [Contributors:]
 *   2001      [eac]   Evgeny A. Cherkashin  <eugeneai@icc.ru>
 *   2001-2002 [janez] Janez Jere            <janez.jere@void.si>
 */

static int TransactionTracker_add(TransactionTracker **list_slot,
    Transaction *cont
  );
static int TransactionTracker_remove(TransactionTracker **list_slot,
    Transaction *cont, boolean
  );

static int Transaction_ensure_active(Transaction *self, PyObject *py_tpb);

static void Transaction_stats_clear(Transaction *self);

static int Transaction_close_with_unlink(
    Transaction *self, boolean allowed_to_raise
  );
static int Transaction_close_without_unlink(
    Transaction *self, boolean allowed_to_raise
  );

static PyObject *pyob_Transaction_cursor(Transaction *self);
static PyObject *pyob_Transaction_convert_and_validate_tpb(
    PyObject *py_tpb_raw
  );

#define Transaction_has_been_untracked(trans) ((trans)->con == NULL)

#ifdef ENABLE_CONNECTION_TIMEOUT
  #define TRANS_REQUIRE_OPEN_(self, failure_action) \
    if (!Transaction_is_not_closed(self)) { \
      if (Transaction_con_timed_out(self)) { \
        raise_exception(ConnectionTimedOut, "This Transaction's Connection" \
            " timed out; the Transaction can no longer be used." \
          ); \
      } else { \
        raise_exception(ProgrammingError, \
            "I/O operation on closed Transaction" \
          ); \
      } \
      failure_action; \
    } else { \
      /* If the transaction claims it's open, verify that its con and \
       * con_python_wrapper members are not NULL: */ \
      assert ((self)->con != NULL); \
      assert ((self)->con_python_wrapper != NULL); \
    }
#else
  #define TRANS_REQUIRE_OPEN_(self, failure_action) \
    if (!Transaction_is_not_closed(self)) { \
      raise_exception(ProgrammingError, \
          "I/O operation on closed Transaction" \
        ); \
      failure_action; \
    }
#endif

#define TRANS_REQUIRE_OPEN(self) TRANS_REQUIRE_OPEN_(self, return NULL)

/************* Transaction INITIALIZATION AND DESTRUCTION:BEGIN **************/
static void Transaction_struct_raw_init(Transaction *self) {
  /* Nullify all of the self's fields first, so that if one of the field
   * initializations that requires additional allocation fails, the cleanup
   * code can check each field without fear of referring to uninitialized
   * memory. */

  self->state = TR_STATE_CREATED;

  self->con = NULL;
  self->con_python_wrapper = NULL;

  self->trans_handle = NULL_TRANS_HANDLE;
  self->group = NULL;

  self->default_tpb = NULL;

  self->open_cursors = NULL;
  self->open_blobreaders = NULL;

  self->n_physical_transactions_started = 0;
  self->n_prepared_statements_executed_since_current_phys_start = 0;
} /* Transaction_struct_raw_init */

static PyObject *pyob_Transaction_new(PyTypeObject *subtype,
    PyObject *args, PyObject *kwargs
  )
{
  Transaction *self = (Transaction *) subtype->tp_alloc(subtype, 0);
  if (self == NULL) { goto fail; }

  Transaction_struct_raw_init(self);

  return (PyObject *) self;
  fail:
    /* Lack of assert (PyErr_Occurred()) here is deliberate. */
    Py_XDECREF(self);
    return NULL;
} /* pyob_Transaction_new */

static int Transaction_init(Transaction *self,
    PyObject *args, PyObject *kwargs
  )
{
  static char *kwarg_list[] = {"con", "tpb", NULL};

  CConnection *con_owned_ref = NULL;
  CConnection *con_unowned_ref = NULL;
  PyObject *default_tpb_raw = NULL;

  { /* Scope for the ambiguously named object con_wrapper. */
    PyObject *con_wrapper;
    assert (self->state == TR_STATE_CREATED);

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", kwarg_list,
          &con_wrapper, &default_tpb_raw
      ))
    { goto fail; }

    /* Validate the provided connection.  If the caller supplied a CConnection
     * instance directly, instead of its Python wrapper object, then we can
     * short-circuit. */
    if (PyObject_TypeCheck(con_wrapper, &ConnectionType)) {
      Py_INCREF(con_wrapper);
      con_owned_ref = (CConnection *) con_wrapper;
    } else {
      PyObject *con_maybe = PyObject_GetAttr(con_wrapper, shared___s__C_con);
      if (con_maybe == NULL) { goto fail; }

      if (!PyObject_TypeCheck(con_maybe, &ConnectionType)) {
        raise_exception(InternalError, "Transaction_init: _C_con is not of"
            " type ConnectionType."
          );
        Py_DECREF(con_maybe);
        goto fail;
      }
      /* In this case, ownership of the reference is passed into con_owned_ref
       * and from there into self->con.  When it makes the transition from
       * con_owned_ref to self->con, this function loses ownership over the
       * reference, and therefore uses con_unowned_ref to refer to it from that
       * point forward. */
      con_owned_ref = (CConnection *) con_maybe;
    }
  } /* end of scope for the ambiguously named object con_wrapper */

  /* Validate default_tpb_raw and store it in self->default_tpb, if it was
   * specified: */
  assert (self->default_tpb == NULL);
  if (default_tpb_raw != NULL && default_tpb_raw != Py_None) {
    self->default_tpb = pyob_Transaction_convert_and_validate_tpb(
        default_tpb_raw
      );
    if (self->default_tpb == NULL) { goto fail; }
    assert (PyString_CheckExact(self->default_tpb));
  }

  assert (con_owned_ref != NULL);
  CON_ACTIVATE(con_owned_ref, goto fail);
  /* CON_ACTIVATE should've verified this already: */
  assert (con_owned_ref != null_connection);

  assert (con_unowned_ref == NULL);
  self->con = con_unowned_ref = con_owned_ref;
  con_owned_ref = NULL;

  /* Create owned reference to the kinterbasdb.Connection Python wrapper
   * instance that sits on top of CConnection self->con: */
  assert (con_unowned_ref->python_wrapper_obj != NULL);
  Py_INCREF(con_unowned_ref->python_wrapper_obj);
  self->con_python_wrapper = con_unowned_ref->python_wrapper_obj;
  /* self->con_python_wrapper is supposed to be an instance of Python class
   * kinterbasdb.Connection, not an instance of CConnection: */
  assert (!PyObject_TypeCheck(self->con_python_wrapper, &ConnectionType));

  /* Enter self in the connection's Transaction tracker: */
  if (TransactionTracker_add(&con_unowned_ref->transactions, self) != 0) {
    goto fail_with_passivation;
  }

  self->state = TR_STATE_RESOLVED;
  CON_PASSIVATE(con_unowned_ref);

  assert (con_owned_ref == NULL);
  assert (self->con != NULL);
  assert (self->con_python_wrapper != NULL);

  return 0;

  fail_with_passivation:
    assert (PyErr_Occurred());

    assert (con_owned_ref   == NULL);
    assert (con_unowned_ref != NULL);
    CON_PASSIVATE(con_unowned_ref);

    /* Fall through to fail: */
  fail:
    assert (PyErr_Occurred());

    if (con_owned_ref != NULL) {
      assert (con_unowned_ref == NULL);
      Py_DECREF(con_owned_ref);
    }

    return -1;
} /* Transaction_init */

static void Transaction_delete(Transaction *self) {
  /* If the Transaction object succeeded in being completely constructed, then
   * its state will have moved past TR_STATE_CREATED.  If that happened, then
   * the transaction should already have been properly closed by the time it
   * reaches this point: */
  assert (
      self->state == TR_STATE_CREATED
    ? TRUE
    : !Transaction_is_not_closed(self)
  );

  assert (self->con == NULL);
  assert (self->con_python_wrapper == NULL);

  assert (self->trans_handle == NULL_TRANS_HANDLE);
  assert (self->group == NULL);

  if (self->default_tpb != NULL) {
    Py_DECREF(self->default_tpb);
    self->default_tpb = NULL;
  }

  assert (self->open_cursors == NULL);
  assert (self->open_blobreaders == NULL);

  /* No action on the following fields:
   *  - n_physical_transactions_started
   *  - n_prepared_statements_executed_since_current_phys_start */
} /* Transaction_delete */

static void pyob_Transaction___del__(Transaction *self) {
  CConnection *con = self->con;

  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

  if (con != NULL) {
    /* Make sure con stays alive until we're done with it: */
    PyObject *con_python_wrapper = con->python_wrapper_obj;
    boolean should_manipulate_con_refcnt;
    assert (con_python_wrapper != NULL);
    /* If this destructor is being called as a result of the execution of
     * trans's destructor, we most definitely must not manipulate trans's
     * reference count, which would cause trans to be "resurrected" and then
     * for its destructor to execute recursively!
     * Also, if self is con's main_trans, we must not manipulate the reference
     * count. */
    should_manipulate_con_refcnt =
      (con->ob_refcnt != 0 && !Transaction_is_main(self));
    if (should_manipulate_con_refcnt) {
      Py_INCREF(con_python_wrapper);
      Py_INCREF(con);
    }

    {
    #ifdef ENABLE_CONNECTION_TIMEOUT
    const boolean needed_to_acquire_tp = !CURRENT_THREAD_OWNS_CON_TP(con);
    if (needed_to_acquire_tp) {
      ACQUIRE_CON_TP_WITH_GIL_HELD(con);
    }
    /* If the Connection Timeout Thread (CTT) caused this Transaction to be
     * untracked while this thread was waiting for the lock, this thread should
     * not close the Transaction again. */
    if (!Transaction_has_been_untracked(self)) {
    #endif /* ENABLE_CONNECTION_TIMEOUT */
      if (Transaction_is_not_closed(self)) {
        /* Close self, in the process removing self from the connection's
         * Transaction tracker: */
        assert (self->con != NULL);
        assert (self->con->transactions != NULL);
        Transaction_close_with_unlink(self, FALSE);
      } else {
        /* self won't be in the connection's Transaction tracker because self
         * has already been moved to a non-open state. */
        assert (self->con == NULL);
        assert (self->con_python_wrapper == NULL);
        Transaction_close_without_unlink(self, FALSE);
      }
      assert (self->con == NULL);
      assert (self->con_python_wrapper == NULL);
      assert (self->trans_handle == NULL_TRANS_HANDLE);
      assert (self->group == NULL);
      assert (self->open_cursors == NULL);
      assert (self->open_blobreaders == NULL);
    #ifdef ENABLE_CONNECTION_TIMEOUT
    }
    if (needed_to_acquire_tp) {
      RELEASE_CON_TP(con);
    }
    #endif /* ENABLE_CONNECTION_TIMEOUT */
    }

    if (should_manipulate_con_refcnt) {
      Py_DECREF(con);
      Py_DECREF(con_python_wrapper);
    }
    con = NULL;
  }

  Transaction_delete(self); /* Low-level deletion of members. */

  /* Release the Transaction struct itself: */
  self->ob_type->tp_free((PyObject *) self);
} /* pyob_Transaction___del__ */
/************** Transaction INITIALIZATION AND DESTRUCTION:END ***************/

/************** Transaction METHODS INACCESSIBLE TO PYTHON:BEGIN *************/
static CConnection *Transaction_get_con(Transaction *trans) {
  assert (trans != NULL);
  return trans->con;
} /* Transaction_get_con */

static PyObject *Transaction_get_con_python_wrapper(Transaction *trans) {
  assert (trans != NULL);
  /* trans->con and trans->con_python_wrapper should be maintained in
   * tight synch: */
  assert (
        trans->con != NULL
      ? trans->con_python_wrapper != NULL
      : trans->con_python_wrapper == NULL
    );
  return trans->con_python_wrapper;
} /* Transaction_get_con_python_wrapper */

static isc_tr_handle *Transaction_get_handle_p(Transaction *self) {
  assert (self != NULL);
  /* Note that the GIL must be held when the function is called. */
  if (self->trans_handle != NULL_TRANS_HANDLE) {
    assert (self->group == NULL);
    return &self->trans_handle;
  } else {
    PyObject *group = self->group;
    isc_tr_handle *native_handle_addr = NULL;

    if (group != NULL) {
      PyObject *py_trans_handle = PyObject_GetAttr(group,
          trans___s__trans_handle
        );
      if (py_trans_handle == NULL) { goto fail; }
      /* The Python layer should not allow this function to be called if the
       * ConnectionGroup has not yet established a transaction handle. */
      assert (py_trans_handle != Py_None);
      if (!StandaloneTransactionHandle_Check(py_trans_handle)) {
        raise_exception(InternalError, "ConnectionGroup._trans_handle should"
            " be a StandaloneTransactionHandle object."
          );
        Py_DECREF(py_trans_handle);
        goto fail;
      }

      native_handle_addr =
        &((StandaloneTransactionHandle *) py_trans_handle)->native_handle;
      /* Obviously, this function assumes that the caller will not use the
       * returned address after the death of the StandaloneTransactionHandle
       * object. */
      Py_DECREF(py_trans_handle);
    }

    return native_handle_addr;
  }

  assert (FALSE); /* Should never reach this point. */
  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* Transaction_get_handle_p */

static void Transaction_reconsider_state(Transaction *self) {
  /* Although a Transaction normally keeps its state and trans_handle members
   * consistent, there are ways for them to become inconsistent.  For example,
   *   con.execute_immediate('rollback')
   * could leave con.main_transaction.state == TR_STATE_UNRESOLVED, but
   * con.main_transaction.trans_hanlde == NULL_TRANS_HANDLE.
   * This method is provided so that client code of self that has just executed
   * a statement can give self a chance to bring state and trans_handle back
   * into synch. */
   assert (self != NULL);
   {
     isc_tr_handle *trans_handle_p = Transaction_get_handle_p(self);
     if (trans_handle_p == NULL || *trans_handle_p == NULL_TRANS_HANDLE) {
       if (self->state != TR_STATE_RESOLVED) {
         assert (self->state == TR_STATE_UNRESOLVED);
         self->state = TR_STATE_RESOLVED;
       }
     } else { /* Handle indicates active transaction: */
       if (self->state != TR_STATE_UNRESOLVED) {
         assert (self->state == TR_STATE_RESOLVED);
         self->state = TR_STATE_UNRESOLVED;
       }
     }
   }
} /* Transaction_reconsider_state */

static ISC_STATUS *Transaction_get_sv(Transaction *self) {
  assert (self != NULL);
  /* This shouldn't even be called when the Transaction isn't open, so we
   * validate with assertion instead of exception. */
  assert (Transaction_get_con(self) != NULL);
  return Transaction_get_con(self)->status_vector;
} /* Transaction_get_sv */

static isc_db_handle *Transaction_get_db_handle_p(Transaction *self) {
  assert (self != NULL);
  /* This shouldn't even be called when the Transaction isn't open, so we
   * validate with assertion instead of exception. */
  assert (Transaction_get_con(self) != NULL);
  assert (!Connection_is_closed(Transaction_get_con(self)));
  return &Transaction_get_con(self)->db_handle;
} /* Transaction_get_db_handle_p */

static unsigned short Transaction_get_dialect(Transaction *self) {
  assert (self != NULL);
  assert (Transaction_get_con(self) != NULL);
  assert (!Connection_is_closed(Transaction_get_con(self)));
  return Transaction_get_con(self)->dialect;
} /* Transaction_get_dialect */

static int Transaction_execute_immediate(Transaction *self,
    PyObject *py_sql_raw
  )
{
  int status = -1;
  PyObject *py_sql_as_str = NULL;
  Py_ssize_t sql_len;

  assert (self != NULL);
  assert (py_sql_raw != NULL);

  /* Caller should've already verified that self isn't closed (though it's
   * acceptable if self is not *active* when this function is called): */
  assert (Transaction_is_not_closed(self));
  assert (self->con != NULL);
  /* Caller should've already activated the connection: */
  CON_MUST_ALREADY_BE_ACTIVE(self->con);

  if (PyString_CheckExact(py_sql_raw)) {
    /* The INCREF is logically unnecessary, but we perform it for symmetry with
     * the unicode-converting branch: */
    Py_INCREF(py_sql_raw);
    py_sql_as_str = py_sql_raw;
  } else if (PyUnicode_CheckExact(py_sql_raw)) {
    py_sql_as_str = PyUnicode_AsASCIIString(py_sql_raw);
    if (py_sql_as_str == NULL) { goto fail; }
  } else {
    assert (py_sql_as_str == NULL);
    raise_exception(ProgrammingError, "SQL argument to execute_immediate must"
        " be str."
      );
    goto fail;
  }
  assert (py_sql_as_str != NULL);

  sql_len = PyString_GET_SIZE(py_sql_as_str);
  if (!_check_statement_length(sql_len)) {
    assert (PyErr_Occurred());
    goto fail;
  }

  /* Start a physical transaction, if self doesn't already have one: */
  if (Transaction_ensure_active(self, NULL) != 0) {
    assert (PyErr_Occurred());
    goto fail;
  }

  {
    /* Note that we call Transaction_get_handle_p while holding the GIL: */
    isc_tr_handle *trans_handle_p = Transaction_get_handle_p(self);
    char *sql = PyString_AS_STRING(py_sql_as_str);
    CConnection *con = self->con;
    assert (con != NULL);

    ENTER_GDAL
    isc_dsql_execute_immediate(con->status_vector,
        &con->db_handle,
        trans_handle_p,
        /* Cast is safe because sql_len has already been constrained: */
        (unsigned short) sql_len,
        sql,
        con->dialect,
        NULL
      );
    LEAVE_GDAL

    Transaction_reconsider_state(self);

    if (DB_API_ERROR(con->status_vector)) {
      raise_sql_exception_exc_type_filter(ProgrammingError,
          "isc_dsql_execute_immediate: ", con->status_vector,
          pyob_Cursor_execute_exception_type_filter
        );
      goto fail;
    }
  } /* end of scope surrounding isc_dsql_execute_immediate call */

  assert (!PyErr_Occurred());
  status = 0;
  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (status == -1);
    /* Fall through to clean: */
  clean:
    Py_XDECREF(py_sql_as_str);
    return status;
} /* Transaction_execute_immediate */

static PyObject *pyob_Transaction_execute_immediate(
    Transaction *self, PyObject *args
  )
{
  PyObject *py_res = NULL;
  PyObject *py_sql;
  CConnection *con;

  TRANS_REQUIRE_OPEN(self);
  assert (self->con != NULL);
  con = self->con;
  CON_ACTIVATE(con, return NULL);

  /* We extract py_sql from the args tuple here, but don't validate it.
   * Transaction_execute_immediate will do that. */
  if (!PyArg_ParseTuple(args, "O", &py_sql)) { goto fail; }

  if (Transaction_execute_immediate(self, py_sql) != 0) { goto fail; }

  assert (!PyErr_Occurred());
  py_res = Py_None;
  Py_INCREF(Py_None);
  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (py_res == NULL);
    /* Fall through to clean: */
  clean:
    CON_PASSIVATE(con);
    CON_MUST_NOT_BE_ACTIVE(con);
    return py_res;
} /* pyob_Transaction_execute_immediate */

static PyObject *pyob_Transaction_convert_and_validate_tpb(
    PyObject *py_tpb_raw
  )
{
  /* On success, returns a new reference to a str that contains the rendered
   * TPB.  On failure, sets an exception and returns NULL. */
  PyObject *tpb = PyObject_CallFunctionObjArgs(pyob_validate_tpb,
      py_tpb_raw, NULL
    );
  if (tpb == NULL) {
    assert (PyErr_Occurred());
    goto fail;
  }
  /* Keep in mind that tpb contains an owned reference here. */

  if (!PyString_CheckExact(tpb)) {
    /* tpb isn't a str, so presumably it's a kinterbasdb.TPB instance.  Execute
     * the equivalent of this Python statement:
     *   tpb = tpb.render() */
    {
      PyObject *tpb_str = PyObject_CallMethod(tpb, "render", NULL);
      Py_DECREF(tpb);
      tpb = tpb_str;
    }
    if (tpb == NULL) {
      assert (PyErr_Occurred());
      goto fail;
    } else if (!PyString_CheckExact(tpb)) {
      raise_exception(ProgrammingError, "TPB must be an instance of str or"
          " kinterbasdb.TPB."
        );
      goto fail;
    }
  }

  assert (tpb != NULL);
  assert (PyString_CheckExact(tpb));
  return tpb;

  fail:
    assert (PyErr_Occurred());
    Py_XDECREF(tpb);
    return NULL;
} /* pyob_Transaction_convert_and_validate_tpb */

static PyObject *pyob_Transaction_get_default_tpb(Transaction *self) {
  /* This function "bubbles upward" from the Transaction to the Connection to
   * the kinterbasdb module, returning a new reference to a str (memory buffer)
   * representation of the first non-NULL default TPB that it finds. */

  if (self->default_tpb != NULL) {
    /* The default_tpb member of a Transaction object, unlike the default_tpb
     * member of a Connection object, is always a str: */
    assert (PyString_CheckExact(self->default_tpb));

    Py_INCREF(self->default_tpb);
    return self->default_tpb;
  } else {
    /* The bubbling from the connection to the module level is implicit, since
     * the connection will have inherited the module-level default_tpb if
     * appropriate. */
    PyObject *con_default_tpb = PyObject_GetAttr(self->con_python_wrapper,
        trans___s__default_tpb_str_
      );
    if (con_default_tpb == NULL) { goto fail; }
    /* The _default_tpb_str_ property of the connection should have rendered
     * the TPB to a str if necessary: */
    assert (PyString_CheckExact(con_default_tpb));
    /* PyObject_GetAttr already supplied a new reference; no need to INCREF. */
    return con_default_tpb;
  }

  assert (FALSE); /* Shouldn't reach this point. */
  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* pyob_Transaction_get_default_tpb */

static void Transaction_clear_connection_references(Transaction *self) {
  const boolean is_main = Transaction_is_main(self);

  assert (self->con != NULL);
  if (!is_main) {
    Py_DECREF(self->con);
  }
  self->con = NULL;

  assert (self->con_python_wrapper != NULL);
  if (!is_main) {
    Py_DECREF(self->con_python_wrapper);
  }
  self->con_python_wrapper = NULL;
} /* Transaction_clear_connection_references */

#define define_Transaction_close_tracker(tracker_type, tracker_member_name) \
  static int Transaction_close_ ## tracker_member_name (Transaction *self) { \
    return tracker_type ## _release(&self->tracker_member_name); \
  }
/* Defines Transaction_close_open_cursors(...): */
define_Transaction_close_tracker(CursorTracker, open_cursors)
/* Defines Transaction_close_open_blobreaders(...): */
define_Transaction_close_tracker(BlobReaderTracker, open_blobreaders)

static int Transaction_close_open_blobreaders_ignoring_errors(
    Transaction *self
  )
{
  /* This function is essentially equivalent to
   * Transaction_close_open_blobreaders, except that it doesn't stop when it
   * encounters errors, and in particular, it informs BlobReader_untrack that
   * it is !allowed_to_raise.
   *
   * It would be better to add a 'boolean allowed_to_raise' parameter to
   * *Tracker_release(...), but that's impractical with the current macro-based
   * implementation. */
  int status = 0;
  BlobReaderTracker *br_node = self->open_blobreaders;

  while (br_node != NULL) {
    BlobReader *br = br_node->contained;
    assert (br != NULL);

    if (BlobReader_untrack(br, FALSE /* !allowed_to_raise*/) != 0) {
      status = -1;
      /* There shouldn't be a Python exception, because we ordered
       * BlobReader_untrack not to raise one: */
      assert (!PyErr_Occurred());
    }

    { /* Note that we free br_node as we advance to the next node: */
      BlobReaderTracker *br_node_next = br_node->next;
      kimem_main_free(br_node);
      br_node = br_node_next;
    }
  }

  self->open_blobreaders = NULL;
  return status;
} /* Transaction_close_open_blobreaders_ignoring_errors */

static int Transaction_close_without_unlink(
    Transaction *self, boolean allowed_to_raise
  )
{
  /* The "without unlink" part of this function's name refers to not unlinking
   * Transaction self from self's connection.  It does *not* refer to not
   * unlinking self's dependent objects (BlobReaders and Cursors) from self. */
  int status = 0;

  /* YYY: Note that Transaction_close_open_cursors doesn't really support
   * !allowed_to_raise: */
  if (Transaction_close_open_cursors(self) != 0) {
    HANDLE_ERROR_WHEN_POSSIBLY_NOT_ALLOWED_TO_RAISE(allowed_to_raise)
  }

  if (allowed_to_raise) {
    if (Transaction_close_open_blobreaders(self) != 0) {
      assert (PyErr_Occurred());
      goto fail;
    }
  } else {
    if (Transaction_close_open_blobreaders_ignoring_errors(self) != 0) {
      /* Transaction_close_open_blobreaders_ignoring_errors shouldn't raise a
       * Python exception, even if it encounters an error: */
      assert (!PyErr_Occurred());
      /* Set status to error, but keep going, because !allowed_to_raise: */
      status = OP_RESULT_ERROR;
    }
  }

  if (Transaction_is_active(self)) {
    const TransactionalOperationResult resolution_status =
      Transaction_commit_or_rollback(OP_ROLLBACK, self, FALSE,
          allowed_to_raise
        );
    if (resolution_status != OP_RESULT_OK) {
      if (allowed_to_raise) {
        goto fail;
      } else {
        /* Do the best we can, since we're not allowed to raise exception: */
        self->trans_handle = NULL_TRANS_HANDLE;
        SUPPRESS_EXCEPTION;
      }
    }
  }

  self->state = TR_STATE_CLOSED;
  return status;
  fail:
    assert (PyErr_Occurred());
    return -1;
} /* Transaction_close_without_unlink */

static int Transaction_untrack(Transaction *self, boolean allowed_to_raise) {
  /* We're here because the superior object (a CConnection) ordered a purge of
   * its tracker.
   * Since self might have subordinate objects (Cursors and BlobReaders) that
   * will release their references to their superior object (self), we must
   * ensure that if self becomes eligible for destruction as a result of this
   * untracking operation, self remains alive at least long enough to complete
   * the untracking in an orderly manner.
   * So, note the artifical INCREF(self)/DECREF(self) in this method. */
  int status = -1;

  assert (self != NULL);
  assert (self->ob_refcnt > 0);
  Py_INCREF(self);

  /* Note that we assert Transaction_is_not_closed, but we do not require
   * Transaction_is_active, because Transactions can remain in their trackers
   * even when they don't have an open physical transaction. */
  assert (Transaction_is_not_closed(self));

  if (Transaction_close_without_unlink(self, allowed_to_raise) != 0) {
    if (allowed_to_raise) { goto fail; }
  }
  assert (allowed_to_raise ? !Transaction_is_not_closed(self) : TRUE);
  assert (allowed_to_raise ? !Transaction_is_active(self) : TRUE);

  Transaction_clear_connection_references(self);

  assert (!PyErr_Occurred());
  assert (Transaction_has_been_untracked(self));
  status = 0;
  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (status == -1);
    /* Fall through to clean: */
  clean:
    Py_DECREF(self);
    return status;
} /* Transaction_untrack */

static int Transaction_close_with_unlink(
    Transaction *self, boolean allowed_to_raise
  )
{
  int status = 0;

  assert (self->con != NULL);
  assert (self->con->transactions != NULL);

  if (Transaction_close_without_unlink(self, allowed_to_raise) != 0) {
    HANDLE_ERROR_WHEN_POSSIBLY_NOT_ALLOWED_TO_RAISE(allowed_to_raise);
  }
  assert (!Transaction_is_not_closed(self));

  /* Remove self from the connection's transaction tracker: */
  if (TransactionTracker_remove(&self->con->transactions, self, TRUE) != 0) {
    HANDLE_ERROR_WHEN_POSSIBLY_NOT_ALLOWED_TO_RAISE(allowed_to_raise);
  }

  Transaction_clear_connection_references(self);

  assert (Transaction_has_been_untracked(self));
  return status;
  fail:
    assert (PyErr_Occurred());
    return -1;
} /* Transaction_close_with_unlink */

static int Transaction_ensure_active(Transaction *self, PyObject *py_tpb) {
  /* tpb can be set to NULL to cause this method to use the default TPB. */
  int status = -1;
  PyObject *py_tpb_owned_ref = NULL;
  CConnection *con;

  assert (self != NULL);
  TRANS_REQUIRE_OPEN_(self, goto fail);

  assert (self->con != NULL);
  assert (self->con_python_wrapper != NULL);
  con = self->con;

  #ifdef ENABLE_CONNECTION_TIMEOUT
    /* This function does not activate the connection, so it should only be
     * called when the connection has already been activated: */
    assert (
          Connection_timeout_enabled(con)
        ? con->timeout->state == CONOP_ACTIVE
        : TRUE
      );
  #endif

  if (!Transaction_is_active(self)) {
    if (self->group == NULL) {
      if (py_tpb != NULL) {
        py_tpb_owned_ref = pyob_Transaction_convert_and_validate_tpb(py_tpb);
      } else {
        py_tpb_owned_ref = pyob_Transaction_get_default_tpb(self);
      }
      if (py_tpb_owned_ref == NULL) { goto fail; }
      assert (PyString_CheckExact(py_tpb_owned_ref));

      {
        char *tpb_ptr = PyString_AS_STRING(py_tpb_owned_ref);
        const Py_ssize_t tpb_len = PyString_GET_SIZE(py_tpb_owned_ref);
        self->trans_handle = begin_transaction(
            con->db_handle, tpb_ptr, tpb_len,
            NULL, -1, /* all TEB-related params are null */
            con->status_vector
          );
      }
      if (self->trans_handle == NULL_TRANS_HANDLE) { goto fail; }

      assert (self->state == TR_STATE_RESOLVED);
      self->state = TR_STATE_UNRESOLVED;
    } else {
      if (py_tpb != NULL) {
        raise_exception(ProgrammingError, "Cannot specify custom TPB when"
            " starting a distributed transaction."
          );
        goto fail;
      } else {
        /* Call the 'begin' method of self->group: */
        PyObject *py_ret = PyObject_CallMethod(self->group, "begin", NULL);
        if (py_ret == NULL) { goto fail; }
        Py_DECREF(py_ret);
      }
    }
    ++self->n_physical_transactions_started;
  } /* end of if !Transaction_is_active(self) block */
  assert (Transaction_is_active(self));
  assert (self->group != NULL ? self->trans_handle == NULL_TRANS_HANDLE : TRUE);
  assert (Transaction_get_handle_p(self) != NULL);
  assert (*Transaction_get_handle_p(self) != NULL_TRANS_HANDLE);

  assert (!PyErr_Occurred());
  status = 0;
  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (status == -1);
    /* Fall through to clean: */
  clean:
    Py_XDECREF(py_tpb_owned_ref);
    return status;
} /* Transaction_ensure_active */

static TransactionalOperationResult Transaction_commit_or_rollback(
    const WhichTransactionOperation op, Transaction *self,
    const boolean retaining, const boolean allowed_to_raise
  )
{
  TransactionalOperationResult status = OP_RESULT_OK;

  assert (self != NULL);
  assert (self->con != NULL);

  /* Either the Connection should have been marked active, if this method is
   * not being called from the CTT, or its TP should be held, if this method
   * is being called from the CTT.
   *
   * The fact that we call a method of self->group if self->group != NULL, and
   * the group might call a method of self->con that attempts to acquire the
   * lock again, is not a problem, because Connections that have timeout
   * enabled are forbidden from joining ConnectionGroup. */
  #ifndef NDEBUG
    if (!RUNNING_IN_CONNECTION_TIMEOUT_THREAD) {
      CON_MUST_ALREADY_BE_ACTIVE(self->con);
    } else {
      assert (!allowed_to_raise);
      assert (CURRENT_THREAD_OWNS_CON_TP(self->con));
    }
  #endif

  assert (Transaction_is_active(self));
  assert (Transaction_get_handle_p(self) != NULL);
  assert (*Transaction_get_handle_p(self) != NULL_TRANS_HANDLE);

  if (allowed_to_raise) {
    if (Transaction_close_open_blobreaders(self) != 0) {
      assert (PyErr_Occurred());
      return OP_RESULT_ERROR;
    }
  } else {
    if (Transaction_close_open_blobreaders_ignoring_errors(self) != 0) {
      /* Transaction_close_open_blobreaders_ignoring_errors shouldn't raise a
       * Python exception, even if it encounters an error: */
      assert (!PyErr_Occurred());
      /* Set status to error, but keep going, because !allowed_to_raise: */
      status = OP_RESULT_ERROR;
    }
  }

  if (self->group != NULL) {
    assert (self->trans_handle == NULL_TRANS_HANDLE);
    /* Since connections with timeout enabled aren't allowed to participate in
     * distributed transactions, there's no danger that calling a method of
     * the group will cause an attempt to activate the connection again before
     * we've deactivated it. */
    assert (!Connection_timeout_enabled(self->con));
    {
      /* Call the appropriate method of self->group: */
      const char *method_name = (op == OP_COMMIT ? "commit" : "rollback");
      PyObject *py_ret = PyObject_CallMethod(self->group,
          (char *) method_name, NULL
        );
      if (py_ret != NULL) {
        Py_DECREF(py_ret);
        status = OP_RESULT_OK;
      } else {
        if (allowed_to_raise) {
          assert (PyErr_Occurred());
          return OP_RESULT_ERROR;
        } else {
          /* Set error return code, but supress Python exception and keep
           * going: */
          status = OP_RESULT_ERROR;
          SUPPRESS_EXCEPTION;
        }
      }
    }
  } else {
    switch (op) {
      case OP_COMMIT:
        status = commit_transaction(Transaction_get_handle_p(self),
            retaining, self->con->status_vector
          );
        break;

      case OP_ROLLBACK:
        status = rollback_transaction(Transaction_get_handle_p(self),
            retaining, TRUE, self->con->status_vector
          );
        break;
    }
    if (status == OP_RESULT_ERROR && !allowed_to_raise) {
      /* Allow error return code, but supress Python exception and keep
       * going: */
      SUPPRESS_EXCEPTION;
    }
  }

  if (status == OP_RESULT_OK) {
    if (!retaining) {
      self->trans_handle = NULL_TRANS_HANDLE;
      Transaction_stats_clear(self);
      self->state = TR_STATE_RESOLVED;
      assert (!Transaction_is_active(self));
    }
  }

  return status;
} /* Transaction_commit_or_rollback */

static PyObject *_pyob_Transaction_commit_or_rollback(
    const WhichTransactionOperation op, Transaction *self,
    PyObject *args, PyObject *kwargs
  )
{
  /* NOTE:  Although this method normally activates/passivates self->con, it
   * must parse the arguments and make a decision on the basis of their values
   * before it can know whether it can even requires that self be open.  That
   * is necessary because the DB API requires that .commit() and .rollback() be
   * accepted even when there is no transaction.
   * All failure (or success) exits up until the CON_ACTIVATE call should exit
   * via 'return' rather than via 'goto', but all thereafter should pass
   * through clean:, in order to passivate the connection. */
  PyObject *py_res = NULL;
  static char *kwarg_list[] = {"retaining", "savepoint", NULL};
  int retaining_int = FALSE;
  PyObject *py_savepoint_name = NULL;

  assert (self != NULL);

  /* Some internal callers pass NULL for args and kwargs, which we should
   * interpret as a request to use the default options: */
  if (args == NULL && kwargs == NULL) {
    assert (retaining_int == FALSE);
    assert (py_savepoint_name == NULL);
  } else {
    PyObject *py_retaining = Py_False;
    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OO", kwarg_list,
          &py_retaining, &py_savepoint_name
      ))
    { return NULL; }

    retaining_int = PyObject_IsTrue(py_retaining);
    if (retaining_int == -1) { return NULL; }

    if (py_savepoint_name == Py_None) {
      /* Py_None and NULL are equivalent in this context. */
      py_savepoint_name = NULL;
    }

    if (py_savepoint_name != NULL && !PyString_CheckExact(py_savepoint_name)) {
      raise_exception(ProgrammingError, "Savepoint name must be a str.");
      return NULL;
    }
  } /* end of use-default-args vs. validate-supplied-args block */

  if (!Transaction_is_active(self)) {
    /* DB API standard requires that a commit or rollback on a nonexistent
     * transaction succeed.  That does *not* apply to rollback-to-savepoint,
     * however.  Rolling back a transaction that doesn't exist is excusable,
     * but it would be dangerous to allow the user to rollback to a nonexistent
     * savepoint. */
    if (py_savepoint_name != NULL) {
      /* We've already ensured that py_savepoint_name is a str. */
      PyObject *py_err_msg = PyString_FromFormat(
          "Cannot roll back to savepoint \"%s\", because there is no active"
          " transaction.", PyString_AS_STRING(py_savepoint_name)
        );
      if (py_err_msg != NULL) {
        raise_exception(ProgrammingError, PyString_AS_STRING(py_err_msg));
        Py_DECREF(py_err_msg);
      }
      return NULL;
    } else {
      assert (self->trans_handle == NULL_TRANS_HANDLE);
      RETURN_PY_NONE;
    }
  }

  TRANS_REQUIRE_OPEN(self);
  assert (self->con != NULL);
  CON_ACTIVATE(self->con, return NULL);

  /* The implementation of .rollback(savepoint=...) is totally different from
   * that of a normal commit or rollback. */
  if (op == OP_ROLLBACK && py_savepoint_name != NULL) {
    /* PyString_Concat will steal a ref to py_sql and store a new reference to
     * the concatenated str in py_sql. */
    PyObject *py_sql = trans___s_ROLLBACK_TO_SPACE; /* Immortal str. */
    Py_INCREF(py_sql);
    /* The type of py_savepoint_name should've been validated earlier: */
    assert (PyString_CheckExact(py_savepoint_name));
    PyString_Concat(&py_sql, py_savepoint_name);
    if (py_sql == NULL) {
      goto fail;
    } else {
      const int exec_res = Transaction_execute_immediate(self, py_sql);
      Py_DECREF(py_sql);
      if (exec_res != 0) { goto fail; }
    }
  } else {
    const boolean is_retaining = (boolean) retaining_int;
    const TransactionalOperationResult trans_op_res =
      Transaction_commit_or_rollback(op, self, is_retaining,
          TRUE /* allowed_to_raise */
        );
    if (trans_op_res != OP_RESULT_OK) { goto fail; }
    #ifndef NDEBUG
      if (!is_retaining) {
        assert (!Transaction_is_active(self));
        assert (self->trans_handle == NULL_TRANS_HANDLE);
      } else {
        assert (Transaction_is_active(self));
        assert (Transaction_get_handle_p(self) != NULL);
        assert (*Transaction_get_handle_p(self) != NULL_TRANS_HANDLE);
      }
    #endif /* assertion block */
  } /* end of "is it rollback-to-savepoint or other-resolution-op"? block */

  assert (!PyErr_Occurred());
  Py_INCREF(Py_None);
  py_res = Py_None;
  goto clean;

  fail:
    assert (PyErr_Occurred());
    assert (py_res == NULL);
    /* Fall through to clean: */

  clean:
    CON_PASSIVATE(self->con);
    CON_MUST_NOT_BE_ACTIVE(self->con);

    return py_res;
} /* _pyob_Transaction_commit_or_rollback */

static TransactionalOperationResult
  Transaction_rollback_without_connection_activation(
    Transaction *self, boolean allowed_to_raise
  )
{
  assert (self != NULL);
  {
    /* Note that this method DOES NOT activates or deactivate the
     * connection. */
    TransactionalOperationResult status = Transaction_commit_or_rollback(
        OP_ROLLBACK, self, FALSE, allowed_to_raise
      );
    if (status != OP_RESULT_OK && !allowed_to_raise) {
      SUPPRESS_EXCEPTION;
    }
    return status;
  }
} /* Transaction_rollback_without_connection_activation */

static void Transaction_stats_record_ps_executed(Transaction *self) {
  assert (self != NULL);
  ++self->n_prepared_statements_executed_since_current_phys_start;
} /* Transaction_stats_record_ps_executed */

static LONG_LONG Transaction_stats_n_executed_since_phys_start(
    Transaction *self
  )
{
  assert (self != NULL);
  return self->n_prepared_statements_executed_since_current_phys_start;
} /* Transaction_stats_n_executed_since_phys_start */

static void Transaction_stats_clear(Transaction *self) {
  assert (self != NULL);
  self->n_prepared_statements_executed_since_current_phys_start = 0;
} /* Transaction_stats_clear */
/*************** Transaction METHODS INACCESSIBLE TO PYTHON:END **************/

/*************** Transaction METHODS ACCESSIBLE TO PYTHON:BEGIN **************/
static PyObject *pyob_Transaction_close(Transaction *self) {
  PyObject *res = NULL;
  CConnection *con;
  assert (self != NULL);
  con = self->con;

  TRANS_REQUIRE_OPEN(self);

  if (Transaction_is_main(self)) {
    raise_exception(ProgrammingError, "A Connection's main_transaction cannot"
        " be close()d independently of the Connection itself."
      );
    return NULL;
  }

  assert (con != NULL);
  /* We need to retain a reference to con until the very end of this function,
   * so that we know the connection won't be garbage collected before we can
   * unlock its TP lock. */
  Py_INCREF(con);

  #ifdef ENABLE_CONNECTION_TIMEOUT
    ACQUIRE_CON_TP_WITH_GIL_HELD(con);
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  if (Transaction_close_with_unlink(self, TRUE) != 0) { goto fail; }
  /* Although we hold a reference to the connection in a local variable within
   * this function, the reference held as a member of the Transaction object
   * should've been cleared: */
  assert (self->con == NULL);

  res = Py_None;
  Py_INCREF(Py_None);

  goto clean;
  fail:
    assert (PyErr_Occurred());
    /* Fall through to clean: */
  clean:
    #ifdef ENABLE_CONNECTION_TIMEOUT
      RELEASE_CON_TP(con);
    #endif /* ENABLE_CONNECTION_TIMEOUT */
    Py_DECREF(con);
    return res;
} /* pyob_Transaction_close */

static PyObject *pyob_Transaction_cursor(Transaction *self) {
  /* Perform the equivalent of the following Python statement:
   *   return Cursor(self) */
  /* Note that we don't TRANS_REQUIRE_OPEN(self) here, because the Cursor
   * constructor must check that condition anyway. */
  PyObject *py_cur;
  #ifndef NDEBUG
    Py_ssize_t self_orig_refcount = self->ob_refcnt;
  #endif
  py_cur = PyObject_CallFunctionObjArgs((PyObject *) &CursorType,
      self, NULL
    );

  /* Validate the handling by the Cursor constructor of self's reference
   * count: */
  assert (
        py_cur != NULL
      ? self->ob_refcnt == self_orig_refcount + 1
      : self->ob_refcnt == self_orig_refcount
    );

  return py_cur;
} /* pyob_Transaction_cursor */

static PyObject *pyob_Transaction_begin(Transaction *self,
    PyObject *args, PyObject *kwargs
  )
{
  static char *kwarg_list[] = {"tpb", NULL};

  PyObject *py_res = NULL;
  PyObject *py_tpb = NULL;
  CConnection *con;

  assert (self != NULL);
  TRANS_REQUIRE_OPEN(self);

  assert (self->con != NULL);
  con = self->con;

  CON_ACTIVATE(con, return NULL);

  if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwarg_list,
        &py_tpb
    ))
  { goto fail; }
  /* Py_None and NULL are equivalent in this context: */
  if (py_tpb == Py_None) {
    py_tpb = NULL;
  }

  /* Raise a more informative error message if the previous transaction is
   * still active when the client attempts to start another.  The old approach
   * was to go ahead and try to start the new transaction regardless.  If there
   * was already an active transaction, the resulting exception made no mention
   * of it, which was very confusing. */
  if (Transaction_is_active(self)) {
    raise_exception_with_numeric_error_code(ProgrammingError, -901,
        "Previous transaction still active; cannot start new transaction."
        "  Use commit() or rollback() to resolve the old transaction first."
      );
    goto fail;
  }

  if (Transaction_ensure_active(self, py_tpb) != 0) {
    assert (PyErr_Occurred());
    goto fail;
  }
  assert (Transaction_is_active(self));

  Py_INCREF(Py_None);
  py_res = Py_None;

  goto clean;
  fail:
    assert (PyErr_Occurred());
    /* Fall through to clean: */
  clean:
    CON_PASSIVATE(con);
    CON_MUST_NOT_BE_ACTIVE(con);
    return py_res;
} /* pyob_Transaction_begin */

static PyObject *pyob_Transaction_prepare(Transaction *self) {
  PyObject *py_res = NULL;
  CConnection *con;

  assert (self != NULL);
  TRANS_REQUIRE_OPEN(self);
  assert (self->con != NULL);
  con = self->con;

  CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(con, return NULL);

  if (self->group == NULL) {
    if (   prepare_transaction(&self->trans_handle, con->status_vector)
        != OP_RESULT_OK
       )
    { goto fail; }
  } else {
    /* Call the 'prepare' method of self->group: */
    PyObject *py_ret = PyObject_CallMethod(self->group, "prepare", NULL);
    if (py_ret == NULL) { goto fail; }
    Py_DECREF(py_ret);
  }

  assert (!PyErr_Occurred());
  py_res = Py_None;
  Py_INCREF(Py_None);

  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (py_res == NULL);
    /* Fall through to clean: */
  clean:
    CON_PASSIVATE(con);
    CON_MUST_NOT_BE_ACTIVE(con);
    return py_res;
} /* pyob_Transaction_prepare */

static PyObject *pyob_Transaction_commit(Transaction *self,
    PyObject *args, PyObject *kwargs
  )
{
  return _pyob_Transaction_commit_or_rollback(OP_COMMIT, self, args, kwargs);
} /* pyob_Transaction_commit */

static PyObject *pyob_Transaction_savepoint(Transaction *self, PyObject *args)
{
  PyObject *py_res = NULL;
  PyObject *py_sp_name = NULL;
  PyObject *py_sql = NULL;

  TRANS_REQUIRE_OPEN(self);
  assert (self->con != NULL);
  CON_ACTIVATE(self->con, return NULL);

  if (!PyArg_ParseTuple(args, "O!", &PyString_Type, &py_sp_name)) {
    goto fail;
  }

  /* PyString_Concat will steal a ref to py_sql and store a new reference to
   * the concatenated str in py_sql. */
  py_sql = trans___s_SAVEPOINT_SPACE; /* Immortal str. */
  Py_INCREF(py_sql);
  PyString_Concat(&py_sql, py_sp_name);
  if (py_sql == NULL) { goto fail; }

  if (Transaction_execute_immediate(self, py_sql) != 0) { goto fail; }

  assert (!PyErr_Occurred());
  Py_INCREF(Py_None);
  py_res = Py_None;
  goto clean;

  fail:
    assert (PyErr_Occurred());
    assert (py_res == NULL);
    /* Fall through to clean: */
  clean:
    Py_XDECREF(py_sql);

    CON_PASSIVATE(self->con);
    CON_MUST_NOT_BE_ACTIVE(self->con);
    return py_res;
} /* pyob_Transaction_savepoint */

static PyObject *pyob_Transaction_rollback(Transaction *self,
    PyObject *args, PyObject *kwargs
  )
{
  return _pyob_Transaction_commit_or_rollback(OP_ROLLBACK, self, args, kwargs);
} /* pyob_Transaction_rollback */

static PyObject *pyob_Transaction_transaction_info(Transaction *self,
    PyObject *args
  )
{
  /* Note that we call pyob_Connection_x_info, which activates the connection,
   * so we mustn't do so explicitly. */
  PyObject *py_res = NULL;
  PyObject *args_with_con_prepended = NULL;

  TRANS_REQUIRE_OPEN(self);
  if (!Transaction_is_active(self)) {
    raise_exception(ProgrammingError, "Transaction must be active to issue"
        " info queries."
      );
    return NULL;
  }

  assert (PyTuple_CheckExact(args));
  {
    const Py_ssize_t args_len = PyTuple_GET_SIZE(args);

    args_with_con_prepended = PyTuple_New(args_len + 1);
    if (args_with_con_prepended == NULL) { goto fail; }

    {
      PyObject *con = (PyObject *) self->con;
      assert (con != NULL);
      Py_INCREF(con); /* PyTuple_SET_ITEM steals a reference. */
      PyTuple_SET_ITEM(args_with_con_prepended, 0, con);
    }

    {
      Py_ssize_t i;
      for (i = 0; i < args_len; ++i) {
        PyObject *el = PyTuple_GET_ITEM(args, i);
        Py_INCREF(el); /* PyTuple_SET_ITEM steals a reference. */
        PyTuple_SET_ITEM(args_with_con_prepended, i+1, el);
      }
    }
  }
  assert (args_with_con_prepended != NULL);
  assert (PyTuple_CheckExact(args_with_con_prepended));
  assert (PyTuple_GET_SIZE(args_with_con_prepended)
       == PyTuple_GET_SIZE(args) + 1
    );

  py_res = pyob_Connection_x_info(
      FALSE, /* Requesting transaction info, not database info. */
      &self->trans_handle,
      NULL, args_with_con_prepended
    );
  if (py_res == NULL) { goto fail; }

  assert (!PyErr_Occurred());
  assert (py_res != NULL);
  goto clean;
  fail:
    assert (PyErr_Occurred());
    if (py_res != NULL) {
      Py_DECREF(py_res);
      py_res = NULL;
    }
    /* Fall through to clean: */
  clean:
    Py_XDECREF(args_with_con_prepended);
    return py_res;
} /* pyob_Transaction_transaction_info */

static PyObject *pyob_Transaction_trans_info(Transaction *self, PyObject *args)
{
  /* This method is just a pass-through to Python function _trans_info in
   * __init__.py, which is accessible at the C level via pyob_trans_info.
   *
   * _trans_info calls self.transaction_info, which indirectly activates the
   * connection, so we mustn't do so explicitly. */
  PyObject *py_res = NULL;

  TRANS_REQUIRE_OPEN(self);

  /* Validate the args tuple.  It's supposed to contain a single element, the
   * request. */
  assert (PyTuple_CheckExact(args));
  if (PyTuple_GET_SIZE(args) != 1) {
    raise_exception(ProgrammingError, "trans_info requires exactly one"
        " argument, which can be either a sequence of integer request codes,"
        " or a single integer request code."
      );
    goto fail;
  }

  py_res = PyObject_CallFunctionObjArgs(pyob_trans_info,
      self, PyTuple_GET_ITEM(args, 0), NULL
    );
  if (py_res == NULL) { goto fail; }

  assert (!PyErr_Occurred());
  assert (py_res != NULL);
  return py_res;
  fail:
    assert (PyErr_Occurred());
    Py_XDECREF(py_res);
    return NULL;
} /* pyob_Transaction_trans_info */

static void Transaction_dist_trans_indicate_resultion(
    Transaction *self, PyObject *group, const boolean is_resolved
  )
{
  assert (self != NULL);
  /* A Transaction's own trans_handle should always remain null when it's
   * participating in distributed transactions. */
  assert (self->trans_handle == NULL_TRANS_HANDLE);

  assert (self->group != NULL);
  assert (self->group == group);

  if (is_resolved) {
    assert (self->state == TR_STATE_UNRESOLVED);

    self->state = TR_STATE_RESOLVED;
    Transaction_stats_clear(self);
  } else {
    assert (self->state == TR_STATE_RESOLVED);

    self->state = TR_STATE_UNRESOLVED;
  }
} /* Transaction_dist_trans_indicate_resultion */
/**************** Transaction METHODS ACCESSIBLE TO PYTHON:END ***************/

/**************** Transaction ATTRIBUTE GET/SET METHODS:BEGIN ****************/
static PyObject *pyob_Transaction_connection_get(Transaction *self,
    void *closure
  )
{
  PyObject *py_res = Py_None;

  if (Transaction_get_con(self) != NULL) {
    assert (self->con_python_wrapper != NULL);
    py_res = self->con_python_wrapper;
  } else {
    assert (self->con_python_wrapper == NULL);
  }

  Py_INCREF(py_res);
  return py_res;
} /* pyob_Transaction_connection_get */

static PyObject *pyob_Transaction_closed_get(Transaction *self, void *closure)
{ return PyBool_FromLong(!Transaction_is_not_closed(self)); }

static PyObject *pyob_Transaction_group_get(Transaction *self, void *closure)
{
  PyObject *group = self->group;

  if (group == NULL) {
    RETURN_PY_NONE;
  } else {
    Py_INCREF(group);
    return group;
  }
} /* pyob_Transaction_group_get */

static int pyob_Transaction_group_set(Transaction *self, PyObject *group,
    void *closure
  )
{
  if (group == Py_None) {
    self->group = NULL;
  } else {
    /* Due to the specifics of the ConnectionGroup class, there should be a
     * trans._set_group(None) call between any trans._set_group(group)
     * calls. */
    if (self->group != NULL) {
      raise_exception(InternalError, "Attempt to set transaction group when"
          " previous setting had not been cleared."
        );
      goto fail;
    }
    /* The ConnectionGroup code always calls trans._set_group(None) when a
     * transaction is removed from its group, including when that removal is
     * invoked implicitly by ConnectionGroup.__del__.  Therefore, the
     * transaction can avoid creating a cycle by never owning a reference to
     * its group, yet knowing that ->group will never refer to a dead
     * object. */
    self->group = group;
  }

  return 0;
  fail:
    assert (PyErr_Occurred());
    return -1;
} /* pyob_Transaction_group_set */

static PyObject *pyob_Transaction_n_physical_get(Transaction *self,
    void *closure
  )
{
  return PythonIntOrLongFrom64BitValue(self->n_physical_transactions_started);
}

static PyObject *pyob_Transaction_resolution_get(Transaction *self,
    void *closure
  )
{
  return PyInt_FromLong(self->state == TR_STATE_UNRESOLVED ? 0 : 1);
} /* pyob_Transaction_resolution_get */

static PyObject *pyob_Transaction_cursors_get(Transaction *self,
    void *closure
  )
{
  TRANS_REQUIRE_OPEN(self);

  return pyob_TrackerToList((AnyTracker *) self->open_cursors);
} /* pyob_Transaction_cursors_get */
/******************* Transaction ATTRIBUTE GET/SET METHODS:END ********************/

/************* Transaction CLASS DEFINITION AND INITIALIZATION:BEGIN **************/

static PyMethodDef Transaction_methods[] = {
    {"close", (PyCFunction) pyob_Transaction_close, METH_NOARGS},

    {"cursor", (PyCFunction) pyob_Transaction_cursor, METH_NOARGS},

    {"_execute_immediate", (PyCFunction) pyob_Transaction_execute_immediate,
        METH_VARARGS
      },

    {"begin", (PyCFunction) pyob_Transaction_begin,
        METH_VARARGS|METH_KEYWORDS
      },

    {"prepare", (PyCFunction) pyob_Transaction_prepare, METH_NOARGS},

    {"commit", (PyCFunction) pyob_Transaction_commit,
        METH_VARARGS|METH_KEYWORDS
      },

    {"savepoint", (PyCFunction) pyob_Transaction_savepoint, METH_VARARGS},

    {"rollback", (PyCFunction) pyob_Transaction_rollback,
        METH_VARARGS|METH_KEYWORDS
      },

    {"transaction_info", (PyCFunction) pyob_Transaction_transaction_info,
        METH_VARARGS
      },
    {"trans_info", (PyCFunction) pyob_Transaction_trans_info, METH_VARARGS},

    {NULL}  /* sentinel */
  };

static PyGetSetDef Transaction_getters_setters[] = {
    {"connection",
        (getter) pyob_Transaction_connection_get,
        NULL,
        "The kinterbasdb.Connection associated with this transaction."
      },

    {"closed",
        (getter) pyob_Transaction_closed_get,
        NULL,
        "Whether the Transaction object has been closed (explicitly or"
        " implicitly)."
      },

    {"n_physical",
        (getter) pyob_Transaction_n_physical_get,
        NULL,
        "Number of physical transactions that have been started via this"
        " Transaction object during its lifetime."
      },

    {"resolution",
        (getter) pyob_Transaction_resolution_get,
        NULL,
        "Zero if this Transaction object is currently managing an open"
        " physical transaction. One if the physical transaction has been"
        " resolved normally. Note that this is an int property rather than a"
        " bool, and is named 'resolution' rather than 'resolved', so that the"
        " non-zero values other than one can be assigned to convey specific"
        " information about the state of the transaction, in a future"
        " implementation."
      },

    {"cursors",
        (getter) pyob_Transaction_cursors_get,
        NULL,
        "List of non-close()d Cursor objects associated with this Transaction."
      },

    /* Package-private property used by kinterbasdb.ConnectionGroup: */
    {"_group",
        (getter) pyob_Transaction_group_get,
        (setter) pyob_Transaction_group_set,
        NULL
      },

    {NULL}  /* sentinel */
  };

PyTypeObject TransactionType = { /* new-style class */
    PyObject_HEAD_INIT(NULL)
    0,                                  /* ob_size */
    "kinterbasdb.Transaction",          /* tp_name */
    sizeof(Transaction),                /* tp_basicsize */
    0,                                  /* tp_itemsize */
    (destructor) pyob_Transaction___del__, /* tp_dealloc */
    0,                                  /* tp_print */
    0,                                  /* tp_getattr */
    0,                                  /* tp_setattr */
    0,                                  /* tp_compare */
    0,                                  /* tp_repr */
    0,                                  /* tp_as_number */
    0,                                  /* tp_as_sequence */
    0,                                  /* tp_as_mapping */
    0,                                  /* tp_hash */
    0,                                  /* tp_call */
    0,                                  /* tp_str */
    0,                                  /* tp_getattro */
    0,                                  /* tp_setattro */
    0,                                  /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_ITER,
                                        /* tp_flags */
    0,                                  /* tp_doc */
    0,		                              /* tp_traverse */
    0,		                              /* tp_clear */
    0,		                              /* tp_richcompare */
    0,		                              /* tp_weaklistoffset */

    0,		                              /* tp_iter */
    0,		                              /* tp_iternext */

    Transaction_methods,                /* tp_methods */
    NULL,                               /* tp_members */
    Transaction_getters_setters,        /* tp_getset */
    0,                                  /* tp_base */
    0,                                  /* tp_dict */
    0,                                  /* tp_descr_get */
    0,                                  /* tp_descr_set */
    0,                                  /* tp_dictoffset */

    (initproc) Transaction_init,        /* tp_init */
    0,                                  /* tp_alloc */
    pyob_Transaction_new,               /* tp_new */
    0,                                  /* tp_free */
    0,                                  /* tp_is_gc */
    0,                                  /* tp_bases */
    0,                                  /* tp_mro */
    0,                                  /* tp_cache */
    0,                                  /* tp_subclasses */
    0                                   /* tp_weaklist */
  };

static int init_kidb_transaction(void) {
  /* TransactionType is a new-style class, so PyType_Ready must be called
   * before its getters and setters will function. */
  if (PyType_Ready(&TransactionType) < 0) { goto fail; }

  return 0;
  fail:
    /* This function is indirectly called by the module loader, which makes no
     * provision for error recovery. */
    return -1;
} /* init_kidb_transaction */
/*********** Transaction CLASS DEFINITION AND INITIALIZATION:END *************/

/** TransactionTracker MEMBER FUNC DEFINITIONS AND SUPPORTING FUNCS: BEGIN ***/
#include "_kisupport_lifo_linked_list.h"

LIFO_LINKED_LIST_DEFINE_BASIC_METHODS_PYALLOC_NOQUAL(
    TransactionTracker, Transaction
  )
/*** TransactionTracker MEMBER FUNC DEFINITIONS AND SUPPORTING FUNCS: END ****/
