/* KInterbasDB Python Package - Implementation of Connection
 *
 * 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 Connection_close(
    CConnection *con, boolean allowed_to_raise, boolean actually_detach
  );

static int Connection_create_main_trans(CConnection *con) {
  assert (con != NULL);
  assert (con->python_wrapper_obj != NULL);
  assert (con->main_trans == NULL);
  {
    Transaction *main_trans = con->main_trans = (Transaction *)
      PyObject_CallFunctionObjArgs((PyObject *) &TransactionType,
          /* Note that we pass a CConnection instance, instead of a
           * kinterbasdb.Connection instance, as a normal caller would: */
          con, NULL
        );
    if (main_trans == NULL) { goto fail; }

    /* Normally, a Transaction owns a reference to its ->con, but the con does
     * not own a reference to its transaction.  In the case of ->main_trans,
     * the relationship is reversed.
     * So, we release main_trans's official reference to con, but don't clear
     * the pointer.  Since con owns a reference to main_trans, it's guaranteed
     * that main_trans will survive as long as con needs to use it. */
    assert (main_trans->con == con);
    assert (Transaction_is_main(main_trans));
    assert (main_trans->con->ob_refcnt > 1);
    Py_DECREF(main_trans->con);
    assert (main_trans->con_python_wrapper->ob_refcnt > 1);
    Py_DECREF(main_trans->con_python_wrapper);
  }

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

static CConnection *Connection_create(void) {
  /* CConnection constructor. */
  CConnection *con = PyObject_New(CConnection, &ConnectionType);
  if (con == NULL) { goto fail; }

  con->state = CON_STATE_CLOSED;

  con->python_wrapper_obj = NULL; /* 2005.06.19 */

  con->dialect = SQL_DIALECT_DEFAULT;
  con->db_handle = NULL_DB_HANDLE;

  /* Wait to perform the relatively complex initialization of main_trans with
   * a real Transaction object until all other fields have been at least
   * minimally initialized. */
  con->main_trans = NULL;
  con->transactions = NULL;

  #if (defined(ENABLE_FIELD_PRECISION_DETERMINATION) && !defined(INTERBASE_7_OR_LATER))
  con->desc_cache = NULL;
  #endif

  con->blob_charset_cache = NULL; /* 2005.06.21 */

  con->type_trans_in = NULL;
  con->type_trans_out = NULL;

  con->output_type_trans_return_type_dict = NULL;

  #ifdef ENABLE_CONNECTION_TIMEOUT
    con->timeout = NULL;
  #endif

  con->dsn = NULL;
  con->dsn_len = -1;
  con->dpb = NULL;
  con->dpb_len = -1;

  /* Now perform complex initializations: */
  assert (!PyErr_Occurred());
  assert (con != NULL);
  return con;

  fail:
    assert (PyErr_Occurred());
    Py_XDECREF(con);
    return NULL;
} /* Connection_create */

static void Connection_delete(CConnection *con) {
  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  { /* hoop-jump for C90's retarded scoping */

  /* Detaches from the database and cleans up the resources used by an existing
   * connection (but does not free the CConnection structure itself;
   * that's the responsibility of pyob_Connection___del__). */
  #ifdef ENABLE_CONNECTION_TIMEOUT
    boolean might_need_to_close = TRUE;

    if (Connection_timeout_enabled(con)) {
      ConnectionOpState achieved_state;
      assert (CURRENT_THREAD_OWNS_CON_TP(con));
      achieved_state = ConnectionTimeoutParams_trans_while_already_locked(
          con->timeout, CONOP_IDLE, CONOP_PERMANENTLY_CLOSED
        );
      if (   achieved_state == CONOP_TIMED_OUT_TRANSPARENTLY
          || achieved_state == CONOP_TIMED_OUT_NONTRANSPARENTLY
         )
      {
        might_need_to_close = FALSE;
      }
    }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  #ifdef ENABLE_CONNECTION_TIMEOUT
  if (might_need_to_close) {
  #endif /* ENABLE_CONNECTION_TIMEOUT */
    if (con->db_handle != NULL_DB_HANDLE) {
      if (Connection_close(con, FALSE, TRUE) == 0) {
        assert (con->db_handle == NULL_DB_HANDLE);
      } else {
        /* Connection_close failed, but since we're garbage collecting and
         * can't reasonably raise an exception, ignore the failure to close,
         * and discard the db_handle anyway. */
        con->db_handle = NULL_DB_HANDLE;
        con->state = CON_STATE_CLOSED;
      }
    }
  #ifdef ENABLE_CONNECTION_TIMEOUT
  } else {
    assert (con->db_handle == NULL_DB_HANDLE);
  }
  #endif /* ENABLE_CONNECTION_TIMEOUT */
  assert(con->state == CON_STATE_CLOSED);

  /* In the normal case, the transaction tracker will have been purged during
   * a call to Connection_close.  However, if the connection timed out, it will
   * have been transitioned to a non-open state without clearing its
   * subordinate trackers.  If so, we need to clear them now: */
  if (con->transactions != NULL) {
    if (TransactionTracker_release(&con->transactions) == 0) {
      assert (con->transactions == NULL);
    } else {
      con->transactions = NULL;
      SUPPRESS_EXCEPTION;
    }
  }

  /* If a main_trans object succeeded in being constructed, it should still be
   * present now, but closed: */
  if (con->main_trans != NULL) {
    assert (con->main_trans->open_cursors == NULL);
    assert (con->main_trans->open_blobreaders == NULL);
    /* This CConnection is about to die, so obviously, the Transaction should
     * not be holding any references to it: */
    assert (con->main_trans->con == NULL);
    assert (con->main_trans->con_python_wrapper == NULL);

    Py_DECREF(con->main_trans);
    con->main_trans = NULL;
  }

  #if (defined(ENABLE_FIELD_PRECISION_DETERMINATION) && !defined(INTERBASE_7_OR_LATER))
  assert(con->desc_cache == NULL);
  #endif
  assert(con->blob_charset_cache == NULL); /* 2005.06.21 */

  Py_XDECREF(con->type_trans_in);
  Py_XDECREF(con->type_trans_out);

  Py_XDECREF(con->output_type_trans_return_type_dict);

  /* There is no need to DECREF python_wrapper_obj, because we never owned a
   * reference to it. */
  con->python_wrapper_obj = NULL;

  if (con->dsn != NULL) { kimem_main_free(con->dsn); }
  if (con->dpb != NULL) { kimem_main_free(con->dpb); }

  } /* hoop-jump for C90's retarded scoping */
} /* Connection_delete */

static void pyob_Connection___del__(CConnection *con) {
  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  { /* hoop-jump for C90's retarded scoping */

  #ifdef ENABLE_CONNECTION_TIMEOUT
    const boolean timeout_enabled = Connection_timeout_enabled(con);
    /*const boolean needed_to_manipulate_tp_lock
      = (Connection_timeout_enabled(con) && !CURRENT_THREAD_OWNS_CON_TP(con));*/

    if (timeout_enabled) {
      ACQUIRE_CON_TP_WITH_GIL_HELD(con);
    }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  Connection_delete(con);

  #ifdef ENABLE_CONNECTION_TIMEOUT
    if (timeout_enabled) {
      TP_UNLOCK(con->timeout);

      ConnectionTimeoutParams_destroy(&con->timeout);
      assert (con->timeout == NULL);
    }
  #endif

  PyObject_Del(con);

  } /* hoop-jump for C90's retarded scoping */
} /* pyob_Connection___del__ */

static PyObject *pyob_Connection_close(PyObject *self, PyObject *args) {
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  if (Connection_close(con, TRUE, TRUE) != 0) { goto fail; }

  assert (!PyErr_Occurred());
  RETURN_PY_NONE;
  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* pyob_Connection_close */

static int Connection_close_(
    CConnection *con, boolean allowed_to_raise, boolean actually_detach,
    boolean should_untrack_con_from_CTM,
    boolean should_clear_subordinate_trackers
  )
{
  /* If the parameter allowed_to_raise is FALSE, this function will refrain
   * from raising a Python exception, and will indicate its status solely via
   * the return code. */
  int status = 0;

  /* The actually_detach parameter is present to support:
   *  1) Situations where the caller would like this function to close
   *     every member of the connection except its db_handle.  (The db_handle
   *     field is still nullified, however.)  This is useful, for example, in
   *     drop_database.
   *  2) A workaround for a Firebird 1.0.x bug related to event handling. */

  #ifdef ENABLE_CONNECTION_TIMEOUT
  if (should_untrack_con_from_CTM) {
    int removal_status;

    /* If timeout is not enabled on this connection, the caller should not have
     * set should_untrack_con_from_CTM to true: */
    assert (Connection_timeout_enabled(con));
    /* The ConnectionTimeoutThread performs the untracking manually (for the
     * sake of efficiency): */
    assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

    /* The current thread should hold con->timeout->lock, and should already
     * have transitioned it to a ->state other than CONOP_IDLE, so that when we
     * release con->timeout->lock before calling CTM_remove (as we must do for
     * deadlock avoidance), the ConnectionTimeoutThread will not try to time
     * con out even as this thread is trying to do so manually. */
    assert (CURRENT_THREAD_OWNS_CON_TP(con));
    assert (con->timeout->state != CONOP_IDLE);

    LEAVE_GIL_WITHOUT_AFFECTING_DB
      TP_UNLOCK(con->timeout);
        removal_status = CTM_remove(con);
      TP_LOCK(con->timeout);
    ENTER_GIL_WITHOUT_AFFECTING_DB

    if (removal_status != 0) {
      if (allowed_to_raise) {
        PyErr_SetString(OperationalError, "Connection was unable to untrack"
            " itself from CTM."
          );
        goto fail;
      } else {
        SUPPRESS_EXCEPTION;
        status = -1;
      }
    }
  }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  /* If the main_trans object exists, and has not been closed, then at least it
   * should be in con->transactions: */
  #ifndef NDEBUG
    if (con->main_trans != NULL) {
      if (Transaction_is_not_closed(con->main_trans)) {
        assert (con->transactions != NULL);
      }
    }
  #endif

  if (con->transactions != NULL) {
    if (should_clear_subordinate_trackers) {
      if (TransactionTracker_release(&con->transactions) == 0) {
        assert (con->transactions == NULL);
        /* Although all Transaction objects should've been purged from the
         * tracker, we should still have a reference to main_trans: */
        assert (con->main_trans != NULL);
        assert (con->main_trans->ob_refcnt >= 1);
        /* And it should have been closed: */
        assert (!Transaction_is_active(con->main_trans));
        assert (!Transaction_is_not_closed(con->main_trans));
      } else {
        if (allowed_to_raise) {
          assert (PyErr_Occurred());
          goto fail;
        } else {
          SUPPRESS_EXCEPTION;
          status = -1;
        }
      }
    } else {
      /* This branch is entered when the ConnectionTimeoutThread times a
       * Connection out. */
      assert (RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
      assert (CURRENT_THREAD_OWNS_CON_TP(con));

      /* Although we aren't clearing con's Transaction tracker (and thus, each
       * Transaction will not clear its Cursor tracker), we must close every
       * PreparedStatement open on all of the Transactions' Cursors, because
       * those PreparedStatements will be unusable even if the Connection is
       * later "resumed" after having been timed out.
       *
       * There are some tricky pitfalls in this operation.  If the only
       * references to a Cursor are held by its non-internal
       * PreparedStatements, then closing those PreparedStatements will cause
       * the Cursor to be collected.  We don't want that collection to occur
       * while we're iterating over the object hierarhcy, so we keep an
       * artificial reference to each Cursor that we process in a list until
       * the process is complete. */
      {
        TransactionTracker *trans_node = con->transactions;
        while (trans_node != NULL) {
          Transaction *trans = trans_node->contained;
          assert (trans != NULL);

          {
            CursorTracker *cur_node = trans->open_cursors;
            while (cur_node != NULL) {
              int cursor_clear_status = -1;
              Cursor *cur = cur_node->contained;
              assert (cur != NULL);

              cursor_clear_status = Cursor_close_prepared_statements(
                  cur, allowed_to_raise,
                  /* Close prepared statements, but don't clear their
                   * references to this Cursor: */
                  FALSE
                );
              if (cursor_clear_status == 0) {
                /* If the prepared statements were successfully closed, then
                 * clear various other cursor member fields by calling
                 * Cursor_clear.
                 * That method will change the cursor's state flag to
                 * CURSOR_STATE_CLOSED, but since we're "cleansing" the cursor
                 * without closing it, we restore the state flag to its original
                 * value. */
                const CursorState orig_state = cur->state;
                cursor_clear_status = Cursor_clear(cur, allowed_to_raise);
                if (cursor_clear_status == 0) {
                  assert (cur->state == CURSOR_STATE_CLOSED);
                  cur->state = orig_state;
                }
              } else {
                /* Cursor_close_prepared_statements raised error. */
                if (allowed_to_raise) {
                  assert (PyErr_Occurred());
                  goto fail;
                } else {
                  status = -1;
                  SUPPRESS_EXCEPTION;
                  /* Note that we ignore this exception and keep going, since,
                   * if !allowed_to_raise, it's important that we try our best
                   * to close all possible PreparedStatements. */
                }
              }
              cur_node = cur_node->next;
            } /* loop over the current Transaction's Cursor tracker nodes */
          }
          trans_node = trans_node->next;
        } /* loop over Transaction tracker nodes */
      } /* end of scope for close-prepared-statements operations */
    } /* end of should_clear_subordinate_trackers if/else statement */
  }

  /* free_field_precision_cache will typically involve calls to
   * isc_dsql_free_statement, so it *must* be performed before the database
   * handle is invalidated. */
  #if (defined(ENABLE_FIELD_PRECISION_DETERMINATION) && !defined(INTERBASE_7_OR_LATER))
  free_field_precision_cache(con->desc_cache,
      (boolean)
        (con->state == CON_STATE_OPEN && con->db_handle != NULL_DB_HANDLE),
      con->status_vector
    );
  con->desc_cache = NULL;
  #endif

  if (con->blob_charset_cache != NULL) {
    Py_DECREF(con->blob_charset_cache);
    con->blob_charset_cache = NULL;
  }

  { /* Roll back any associated transactions that are still active. */
    TransactionTracker *trans_node = con->transactions;
    while (trans_node != NULL) {
      Transaction *trans = trans_node->contained;
      assert (trans != NULL);

      if (Transaction_is_active(trans)) {
        if (Transaction_rollback_without_connection_activation(
              trans, allowed_to_raise
            ) == OP_RESULT_OK
           )
        {
          /* Success; the transaction is dead. */
          assert (!Transaction_is_active(trans));
        } else {
          /* The rollback attempt failed.  If we're *not* allowed to raise
           * Python exception, continue as though the rollback attempt had
           * succeeded.
           * Otherwise, goto fail (the rollback method will already have raised
           * a Python exception). */
          assert (PyErr_Occurred());
          if (allowed_to_raise) {
            assert (PyErr_Occurred());
            goto fail;
          } else {
            SUPPRESS_EXCEPTION;
            status = -1;
          }
        } /* end of did-rollback-succeed block */
      } /* end of is-transaction-active block */

      trans_node = trans_node->next;
    }
  }

  if (con->db_handle != NULL_DB_HANDLE) {
    /* See notes about actually_detach parameter near top of this function. */
    if (!actually_detach) {
      con->db_handle = NULL_DB_HANDLE;
    } else {
      #ifdef ENABLE_CONNECTION_TIMEOUT
        assert (CURRENT_THREAD_OWNS_CON_TP(con));
      #endif /* ENABLE_CONNECTION_TIMEOUT */

      {
        /* This code can be reached when the CTT is timing out a connection.
         * In that case, we want the GIL to remain held during the entire
         * timeout operation. */
        OPEN_LOCAL_GIL_MANIPULATION_INFRASTRUCTURE
        #ifdef ENABLE_CONNECTION_TIMEOUT
        const boolean should_manip_gil =
          NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD;
        if (should_manip_gil) {
        #endif
          LEAVE_GIL_WITHOUT_AFFECTING_DB_AND_WITHOUT_STARTING_CODE_BLOCK
        #ifdef ENABLE_CONNECTION_TIMEOUT
        }
        #endif
        ENTER_GDAL_WITHOUT_LEAVING_PYTHON
        ENTER_GCDL_WITHOUT_LEAVING_PYTHON

        isc_detach_database(con->status_vector, &con->db_handle);

        LEAVE_GCDL_WITHOUT_ENTERING_PYTHON
        LEAVE_GDAL_WITHOUT_ENTERING_PYTHON
        #ifdef ENABLE_CONNECTION_TIMEOUT
        if (should_manip_gil) {
        #endif
          ENTER_GIL_WITHOUT_AFFECTING_DB_AND_WITHOUT_ENDING_CODE_BLOCK
        #ifdef ENABLE_CONNECTION_TIMEOUT
        }
        #endif
        CLOSE_LOCAL_GIL_MANIPULATION_INFRASTRUCTURE
      } /* end of lock manipulation scope */

      if (!DB_API_ERROR(con->status_vector)) {
        assert (con->db_handle == NULL_DB_HANDLE);
      } else {
        if (allowed_to_raise) {
          raise_sql_exception(OperationalError, "Connection_close_: ",
              con->status_vector
            );
          goto fail;
        } else {
          SUPPRESS_EXCEPTION;
          status = -1;
        }
      }
    }
  }

  con->state = CON_STATE_CLOSED;

  /* Since we must honor allowed_to_raise, it's possible for status to be -1
   * here, but even if it is, a Python exception should not be set, if
   * !allowed_to_raise: */
  #ifndef NDEBUG
    if (allowed_to_raise) {
      assert (status == 0 ? !PyErr_Occurred() : !!PyErr_Occurred());
    } else {
      assert (!PyErr_Occurred());
    }
  #endif
  goto clean;

  fail:
    assert (PyErr_Occurred());
    status = -1;
    /* Fall through to clean: */
  clean:
    return status;
} /* Connection_close_ */

static int Connection_close(
    CConnection *con, boolean allowed_to_raise, boolean actually_detach
  )
{
  int res = 0;

  #ifdef ENABLE_CONNECTION_TIMEOUT
    /* This function can be called as an indirect result of a Cursor's
     * destruction.  pyob_Cursor___del__ acquires con's TP lock, so we need to
     * avoid trying to do that again (the attempt would deadlock). */
    const boolean already_owned_tp =
          Connection_timeout_enabled(con)
        ? CURRENT_THREAD_OWNS_TP(con->timeout)
        : TRUE
      ;

    if (!already_owned_tp) {
      ACQUIRE_CON_TP_WITH_GIL_HELD(con);
    }

  if (!Connection_timeout_enabled(con)) {
    assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  #endif
    if (con->state != CON_STATE_CLOSED) {
      res = Connection_close_(con, allowed_to_raise, actually_detach, FALSE,
          TRUE
        );
    } else {
      goto fail_already_closed;
    }
  #ifdef ENABLE_CONNECTION_TIMEOUT
  } else {
    ConnectionTimeoutParams *tp = con->timeout;
    const ConnectionOpState orig_state = tp->state;

    switch (orig_state) {
      case CONOP_IDLE: /* The normal case. */
       assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
       {
        /* DEADLOCK AVOIDANCE:
         * This thread, which is not the ConnectionTimeoutThread, currently
         * holds the GIL and tp->lock, but it does not hold the CTM lock.
         *
         * If we continued to hold tp->lock while we called Connection_close_,
         * which in turn calls CTM_remove (which acquires the CTM lock), then a
         * deadlock could result if the ConnectionTimeoutThread is currently
         * performing a sweep, and is holding the CTM lock while trying to
         * acquire tp->lock to see whether con needs to be timed out.
         *
         * Therefore, we transition tp's state from CONOP_IDLE to CONOP_ACTIVE
         * for the duration of the Connection_close_ call.  Connection_close_
         * will release tp->lock before calling CTM_remove, but if the
         * ConnectionTimeoutThread is waiting to check tp->state, the fact that
         * the state is CONOP_ACTIVE will prevent the ConnectionTimeoutThread
         * from attempting to time con out even as we try to manually close
         * con. */
        ConnectionOpState achieved_state =
          ConnectionTimeoutParams_trans_while_already_locked(
              tp, CONOP_IDLE, CONOP_ACTIVE
            );
        /* This should never happen: */
        if (achieved_state != CONOP_ACTIVE) {
          raise_exception(InternalError, "Connection_close was unable to"
              " change the connection's state before calling"
              " Connection_close_."
            );
          goto fail;
        }

        res = Connection_close_(con, allowed_to_raise, actually_detach, TRUE,
            TRUE
          );

        if (res != 0) {
          goto fail;
        } else {
          achieved_state = ConnectionTimeoutParams_trans_while_already_locked(
              tp, CONOP_ACTIVE, CONOP_PERMANENTLY_CLOSED
            );
          if (achieved_state != CONOP_PERMANENTLY_CLOSED) {
            raise_exception(InternalError, "Connection_close was unable to"
                " change the connection's state after calling"
                " Connection_close_."
              );
            goto fail;
          }
        }

        break;
       }

      case CONOP_ACTIVE:
        raise_exception(InternalError, "[Connection_close] tp->state was"
            " CONOP_ACTIVE."
          );
        goto fail;

      case CONOP_PERMANENTLY_CLOSED:
        goto fail_already_closed;

      case CONOP_TIMED_OUT_TRANSPARENTLY:
      case CONOP_TIMED_OUT_NONTRANSPARENTLY:
        /* The ConnectionTimeoutThread already timed con out; we'll just make
         * the closure permanent. */
        if (ConnectionTimeoutParams_trans_while_already_locked(
              tp, orig_state, CONOP_PERMANENTLY_CLOSED
            ) == CONOP_PERMANENTLY_CLOSED
           )
        {
          assert (res == 0);
        } else {
          raise_exception(InternalError, "Connection_close was unable to"
              " change the connection's state from timed out to permanently"
              " closed."
            );
          goto fail;
        }
        break;
    }
  }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  goto exit;
  fail_already_closed:
    raise_exception(ProgrammingError, "The connection is already closed.");
    /* Fall through to fail: */
  fail:
    assert (PyErr_Occurred());
    res = -1;
    /* Fall through to exit: */
  exit:
    #ifdef ENABLE_CONNECTION_TIMEOUT
      if (!already_owned_tp) {
        RELEASE_CON_TP(con);
      }
    #endif
    return res;
} /* Connection_close */

#ifdef ENABLE_CONNECTION_TIMEOUT
static int Connection_close_from_CTT(volatile CConnection *con) {
  int status = -1;
  BlobReader **blob_readers = NULL;
  Py_ssize_t n_blobreaders = -1;
  PreparedStatement **prepared_statements = NULL;
  Py_ssize_t n_prepared_statements = -1;

  assert (RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  assert (CURRENT_THREAD_OWNS_CON_TP(DV_CCON(con)));

  blob_readers = Connection_copy_BlobReader_pointers(con, &n_blobreaders);
  if (n_blobreaders == -1) { goto fail; }

  prepared_statements = Connection_copy_PreparedStatement_pointers(con,
      &n_prepared_statements
    );
  if (n_prepared_statements == -1) { goto fail; }

  status = Connection_close_(DV_CCON(con),
      FALSE, /* allowed_to_raise: The CTT must close the connection, so we need
              * to do our best to clean up all resources associated with the
              * connection, even if not every step goes smoothly. */
      TRUE, /* actually_detach */
      FALSE, /* should_untrack_con_from_CTM: This thread is the CTT; it will
              * perform the untracking after this function finishes. */
      FALSE /* should_clear_subordinate_trackers is FALSE because we don't want
             * to invoke destructors from the CTT if we can avoid it.
             * Connection_close_ contains logic that will close the subordinate
             * objects to the required extent--but not untrack them--if this
             * is FALSE. */
    );
  if (status != 0) { goto fail; }

  if (n_blobreaders > 0) {
    Connection_former_BlobReaders_flag_timeout_and_free(
        blob_readers, n_blobreaders
      );
    blob_readers = NULL;
  }

  if (n_prepared_statements > 0) {
    Connection_former_PreparedStatements_flag_timeout_and_free(
        prepared_statements, n_prepared_statements
      );
    prepared_statements = NULL;
  }

  assert (status != -1);
  goto clean;

  fail:
    assert (status == -1);
    /* Fall through to clean: */
  clean:
    if (blob_readers != NULL) {
      Connection_former_BlobReader_pointers_free(blob_readers);
    }
    if (prepared_statements != NULL) {
      Connection_former_PreparedStatement_pointers_free(prepared_statements);
    }
    return status;
} /* Connection_close_from_CTT */

static TransactionalOperationResult
  Connection_resolve_all_transactions_from_CTT(
    volatile CConnection *con, const WhichTransactionOperation trans_op
  )
{
  TransactionalOperationResult status = OP_RESULT_OK;
  BlobReader **blob_readers = NULL;
  Py_ssize_t n_blobreaders = -1;

  assert (RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  assert (CURRENT_THREAD_OWNS_CON_TP(DV_CCON(con)));

  blob_readers = Connection_copy_BlobReader_pointers(con, &n_blobreaders);
  if (n_blobreaders == -1) {
    /* Even if the attempt to copy the BlobReader
     * pointers fails, we don't want to give up on our
     * attempt to resolve the transaction. */
    SUPPRESS_EXCEPTION;
  }

  /* Resolve all Transactions in con->transactions. */
  {
    TransactionTracker *trans_node = con->transactions;
    while (trans_node != NULL) {
      Transaction *trans = trans_node->contained;
      assert (trans != NULL);
      if (Transaction_is_active(trans)) {
        const TransactionalOperationResult resolution_result =
          Transaction_commit_or_rollback(trans_op, trans, FALSE,
              FALSE /* !allowed_to_raise */
            );
        if (resolution_result != OP_RESULT_OK) {
          /* Change status to record that at least one transaction resolution
           * attempt failed, but keep going despite that failure: */
          status = OP_RESULT_ERROR;
          SUPPRESS_EXCEPTION;
        }
      }
      trans_node = trans_node->next;
    }
  }

  if (n_blobreaders > 0) {
    Connection_former_BlobReaders_flag_timeout_and_free(
        blob_readers, n_blobreaders
      );
    blob_readers = NULL;
  }

  return status;
} /* Connection_resolve_all_transactions_from_CTT */

static BlobReader **Connection_copy_BlobReader_pointers(
    volatile CConnection *con, Py_ssize_t *count
  )
{
  /* The GIL must be held when this function is called. */
  /* Although the BlobReader objects will not be garbage collected as a
   * result of their connection's timeout, all of the connection's
   * Transactions' BlobReaderTrackers will be cleared.
   *
   * We want future attempts to use any of these BlobReaders to raise
   * ConnectionTimedOut rather than ProgrammingError, so we temporarily store
   * pointers to all of the open BlobReaders that will be removed from the
   * trackers, then (in Connection_former_BlobReaders_flag_timeout_and_free) go
   * back and change their state flag from BLOBREADER_STATE_CLOSED to
   * BLOBREADER_STATE_CONNECTION_TIMED_OUT to record the reason for their
   * closure.
   *
   * Note that this same treatment is *not* applied to a connection's
   * Transactions and their Cursors, because beginning anew to manipulate those
   * objects can, under the right conditions, transparently resume the
   * connection. */
  BlobReader **blob_readers = NULL;
  Py_ssize_t n_blobreaders = 0;
  TransactionTracker *trans_node;
  BlobReaderTracker *br_node;

  /* First, count the total number of BlobReaders open on all Transactions on
   * this Connection: */
  trans_node = con->transactions;
  while (trans_node != NULL) {
    Transaction *trans = trans_node->contained;
    assert (trans != NULL);

    br_node = trans->open_blobreaders;
    while (br_node != NULL) {
      assert (br_node->contained != NULL);
      ++n_blobreaders;
      br_node = br_node->next;
    }

    trans_node = trans_node->next;
  }

  if (n_blobreaders > 0) {
    /* Allocate an array to contain all of the BlobReader pointers, then
     * traverse the Transactions' BlobReader trackers to populate the array. */
    blob_readers = kimem_main_malloc(sizeof(BlobReader *) * n_blobreaders);
    if (blob_readers == NULL) { goto fail; }

    {
      Py_ssize_t i = 0;

      trans_node = con->transactions;
      while (trans_node != NULL) {
        Transaction *trans = trans_node->contained;
        assert (trans != NULL);

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

          blob_readers[i++] = br;

          br_node = br_node->next;
        }

        trans_node = trans_node->next;
      }
    }
  }

  *count = n_blobreaders;
  assert (n_blobreaders == 0 ? blob_readers == NULL : blob_readers != NULL);
  return blob_readers;

  fail:
    assert (PyErr_Occurred());
    if (blob_readers != NULL) {
      Connection_former_BlobReader_pointers_free(blob_readers);
    }
    *count = -1;
    return NULL;
} /* Connection_copy_BlobReader_pointers */

static void Connection_former_BlobReaders_flag_timeout_and_free(
    BlobReader **blob_readers, Py_ssize_t count
  )
{
  Py_ssize_t i;
  for (i = 0; i < count; ++i) {
    assert (blob_readers[i]->state == BLOBREADER_STATE_CLOSED);
    blob_readers[i]->state = BLOBREADER_STATE_CONNECTION_TIMED_OUT;
  }

  Connection_former_BlobReader_pointers_free(blob_readers);
} /* Connection_former_BlobReaders_flag_timeout_and_free */

static PreparedStatement **Connection_copy_PreparedStatement_pointers(
    volatile CConnection *con, Py_ssize_t *count
  )
{
  /* The GIL must be held when this function is called. */
  /* See documentation in Connection_copy_BlobReader_pointers and near the
   * declarations of these functions (in _kinterbasdb.c). */
  PreparedStatement **prepared_statements = NULL;
  Py_ssize_t n_prepared_statements = 0;
  TransactionTracker *trans_node;
  CursorTracker *cur_node;
  PSTracker *ps_node;

  /* First, count the total number of PreparedStatements open on all Cursors
   * on all Transactions on this Connection: */
  trans_node = con->transactions;
  while (trans_node != NULL) {
    Transaction *trans = trans_node->contained;
    assert (trans != NULL);

    cur_node = trans->open_cursors;
    while (cur_node != NULL) {
      Cursor *cur = cur_node->contained;
      assert (cur != NULL);

      ps_node = cur->ps_tracker;
      while (ps_node != NULL) {
        assert (ps_node->contained != NULL);
        ++n_prepared_statements;

        ps_node = ps_node->next;
      }

      cur_node = cur_node->next;
    }

    trans_node = trans_node->next;
  }

  if (n_prepared_statements > 0) {
    /* Allocate an array to contain all of the PreparedStatement pointers, then
     * traverse the PreparedStatements to populate the array. */
    prepared_statements = kimem_main_malloc(
        sizeof(PreparedStatement *) * n_prepared_statements
      );
    if (prepared_statements == NULL) { goto fail; }

    {
      Py_ssize_t i = 0;

      trans_node = con->transactions;
      while (trans_node != NULL) {
        Transaction *trans = trans_node->contained;
        assert (trans != NULL);

        cur_node = trans->open_cursors;
        while (cur_node != NULL) {
          Cursor *cur = cur_node->contained;
          assert (cur != NULL);

          ps_node = cur->ps_tracker;
          while (ps_node != NULL) {
            PreparedStatement *ps = ps_node->contained;
            assert (ps != NULL);

            prepared_statements[i++] = ps;
            ps_node = ps_node->next;
          }

          cur_node = cur_node->next;
        }

        trans_node = trans_node->next;
      }
    }
  }

  *count = n_prepared_statements;
  assert (
        n_prepared_statements == 0
      ? prepared_statements == NULL
      : prepared_statements != NULL
    );
  return prepared_statements;

  fail:
    assert (PyErr_Occurred());
    if (prepared_statements != NULL) {
      Connection_former_PreparedStatement_pointers_free(prepared_statements);
    }
    *count = -1;
    return NULL;
} /* Connection_copy_PreparedStatement_pointers */

static void Connection_former_PreparedStatements_flag_timeout_and_free(
    PreparedStatement **prepared_statements, Py_ssize_t count
  )
{
  Py_ssize_t i;
  for (i = 0; i < count; ++i) {
    assert (prepared_statements[i]->state == PS_STATE_DROPPED);
    prepared_statements[i]->state = PS_STATE_CONNECTION_TIMED_OUT;
  }

  Connection_former_PreparedStatement_pointers_free(prepared_statements);
} /* Connection_former_PreparedStatements_flag_timeout_and_free */
#endif /* ENABLE_CONNECTION_TIMEOUT */

static int Connection_require_open(CConnection *self,
    char *failure_message
  )
{
  /* If self is not an open connection, raises the supplied error message (or a
   * default if no error message was supplied).
   * Returns 0 if the connection was open; -1 if was closed. */
  if (self != NULL && self->state == CON_STATE_OPEN) {
    return 0;
  }

  if (failure_message == NULL) {
    failure_message = "Invalid connection state.  The connection must be"
      " open to perform this operation.";
  }

  raise_exception(ProgrammingError, failure_message);
  return -1;
} /* Connection_require_open */

static int Connection_attach_from_members(CConnection *con
    #ifdef ENABLE_CONNECTION_TIMEOUT
      , ConnectionTimeoutParams *tp
    #endif
  )
{
  assert (con != NULL);
  assert (con->state == CON_STATE_CLOSED);
  assert (con->db_handle == NULL_DB_HANDLE);
  assert (con->dialect > 0);
  assert (con->dsn != NULL);
  assert (con->dsn_len >= 0);
  assert (con->dpb == NULL ? TRUE : con->dpb_len >= 0);

  #ifdef ENABLE_CONNECTION_TIMEOUT
  if (tp != NULL) {
    assert (CURRENT_THREAD_OWNS_TP(tp));
  }
  #endif

  LEAVE_GIL_WITHOUT_AFFECTING_DB
  ENTER_GDAL_WITHOUT_LEAVING_PYTHON
  ENTER_GCDL_WITHOUT_LEAVING_PYTHON

  isc_attach_database(con->status_vector,
      con->dsn_len, con->dsn, &con->db_handle, con->dpb_len, con->dpb
    );

  LEAVE_GCDL_WITHOUT_ENTERING_PYTHON
  LEAVE_GDAL_WITHOUT_ENTERING_PYTHON
  ENTER_GIL_WITHOUT_AFFECTING_DB

  if (DB_API_ERROR(con->status_vector)) {
    raise_sql_exception(OperationalError, "isc_attach_database: ",
        con->status_vector
      );
    goto fail;
  }

  assert (con->db_handle != NULL_DB_HANDLE);
  con->state = CON_STATE_OPEN;

  #ifdef ENABLE_CONNECTION_TIMEOUT
    /* At this point, we know that the connection attempt was successful.  If a
     * timeout was requested for this connection, we now add the connection to
     * the timeout tracker.  It is the responsibility of CTM_add to associate
     * our previously initialized timeout parameter structure with con, iff con
     * is successfully added to the tracker. */
    assert (con->timeout == NULL);
    if (tp != NULL) {
      int status;

      tp->connected_at = time_millis();

      assert (tp->state != CONOP_IDLE);

      LEAVE_GIL_WITHOUT_AFFECTING_DB
      status = CTM_add(con, tp);
      ENTER_GIL_WITHOUT_AFFECTING_DB
      if (status != 0) {
        raise_exception(OperationalError, "[Connection_attach_from_members]"
            " Unsuccessful call to CTM_add."
          );
        goto fail;
      }
    }
    assert (con->timeout == tp);
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  return 0;

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

static int Connection_attach(CConnection *con,
    char *dsn, short dsn_len,
    char *dpb, short dpb_len
    #ifdef ENABLE_CONNECTION_TIMEOUT
      , ConnectionTimeoutParams *tp
    #endif
  )
{
  assert (con != NULL);
  assert (con->state == CON_STATE_CLOSED);
  assert (con->db_handle == NULL_DB_HANDLE);
  assert (con->dialect > 0);

  assert (dsn != NULL);
  assert (dsn_len >= 0);
  assert (dpb == NULL ? TRUE : dpb_len >= 0);

  assert (con->dsn == NULL);
  con->dsn = kimem_main_malloc(dsn_len);
  if (con->dsn == NULL) { goto fail; }
  memcpy(con->dsn, dsn, dsn_len);
  con->dsn_len = dsn_len;

  assert (con->dpb == NULL);
  con->dpb = kimem_main_malloc(dpb_len);
  if (con->dpb == NULL) { goto fail; }
  memcpy(con->dpb, dpb, dpb_len);
  con->dpb_len = dpb_len;

  {
    int status;

    #ifndef ENABLE_CONNECTION_TIMEOUT
      status = Connection_attach_from_members(con);
    #else
      if (tp != NULL) {
        ACQUIRE_TP_WITH_GIL_HELD(tp);
      }
      status = Connection_attach_from_members(con, tp);
      if (tp != NULL) {
        TP_UNLOCK(tp);
      }
    #endif
    if (status != 0) { goto fail; }
  }

  assert (!PyErr_Occurred());
  return 0;

  fail:
    assert (PyErr_Occurred());
    /* If we allocated any memory for con->dsn or con->dpb, con's destructor
     * will release it. */
    return -1;
} /* Connection_attach */

static PyObject *pyob_Connection_connect(PyObject *self, PyObject *args) {
  CConnection *con = NULL;
  PyObject *python_wrapper_obj = NULL;

  /* We will receive pointers to pre-rendered DSN and DPB buffers from the
   * Python level. */
  char *dsn = NULL;
  Py_ssize_t dsn_len = 0;
  char *dpb = NULL;
  Py_ssize_t dpb_len = 0;

  long dialect = 0;
  PyObject *py_timeout = NULL;
  #ifdef ENABLE_CONNECTION_TIMEOUT
    ConnectionTimeoutParams *timeout_params = NULL;
  #endif

  /* Parse and validate the arguments. */
  if (!PyArg_ParseTuple(args, "Os#s#lO",
         &python_wrapper_obj, &dsn, &dsn_len, &dpb, &dpb_len, &dialect,
         &py_timeout
       )
     )
  { goto fail; }

  if (dsn_len > MAX_DSN_SIZE) {
    PyObject *err_msg = PyString_FromFormat(
        "DSN too long (" Py_ssize_t_STRING_FORMAT " bytes out of %d allowed).",
        dsn_len, MAX_DSN_SIZE
      );
    if (err_msg != NULL) {
      raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
      Py_DECREF(err_msg);
    }
    goto fail;
  }

  if (dpb_len > MAX_DPB_SIZE) {
    PyObject *err_msg = PyString_FromFormat(
        "Database parameter buffer too large (" Py_ssize_t_STRING_FORMAT
        " bytes out of %d allowed).",
        dpb_len, MAX_DPB_SIZE
      );
    if (err_msg != NULL) {
      raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
      Py_DECREF(err_msg);
    }
    goto fail;
  }

  /* A negative value for the dialect is not acceptable because the IB/FB API
   * requires an unsigned short. */
  if (dialect < 0 || dialect > USHRT_MAX) {
    PyObject *err_msg = PyString_FromFormat(
        "Connection dialect must be between 0 and %d (%ld is out of range).",
        USHRT_MAX, dialect
      );
    if (err_msg != NULL) {
      raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
      Py_DECREF(err_msg);
    }
    goto fail;
  }

  /* Validate the connection timeout settings, if any. */
  #ifndef ENABLE_CONNECTION_TIMEOUT
    if (py_timeout != Py_None) {
      raise_exception(NotSupportedError, "The connection timeout feature is"
          " disabled in this build."
        );
      goto fail;
    }
  #else
    if (py_timeout == Py_None) {
      assert (timeout_params == NULL);
    } else {
      timeout_params = c_timeout_from_py(py_timeout);
      if (timeout_params == NULL) {
        assert (PyErr_Occurred());
        goto fail;
      }
    }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  /* Create a CConnection struct (this is little more than a memory allocation
   * operation, not the actual point of connection to the database server): */
  con = Connection_create();
  if (con == NULL) { goto fail; }

  /* con->dialect is set to a default value in the Connection_create function,
   * so we only need to change it if we received a dialect argument to this
   * function. */
  if (dialect > 0) {
    con->dialect = (unsigned short) dialect;
  }
  assert (con->dialect > 0);

  /* Yes, it is correct not to INCREF this PyObject * before storing it--see
   * comment about the CConnection.python_wrapper_obj field in
   * _kinterbasdb.h */
  con->python_wrapper_obj = python_wrapper_obj;
  assert (con->python_wrapper_obj != NULL);

  /* IT'S IMPORTANT THAT THE CONNECTION BE ADDED TO THE TIMEOUT TRACKER ONLY AS
   * THE LAST STEP HERE, WHEN THE CONNECTION'S MEMBERS HAVE ALREADY BEEN
   * INITIALIZED! */
  /* The casts from Py_ssize_t to short are no problem because the Py_ssize_t
   * values have already been verified as small enough to fit into a short. */
  if (Connection_attach(con, dsn, (short) dsn_len, dpb, (short) dpb_len
          #ifdef ENABLE_CONNECTION_TIMEOUT
            , timeout_params
          #endif
        ) != 0
      )
  {
    goto fail;
  }
  #ifdef ENABLE_CONNECTION_TIMEOUT
  else {
    /* If the ConnectionTimeoutParams object pointed to by local variable
     * timeout_params has been attached to the CConnection, but an error has
     * occurred, we don't want to free the same ConnectionTimeoutParams object
     * twice, so we nullify the local variable: */
    if (con->timeout == timeout_params) {
      timeout_params = NULL;
    }
  }
  #endif

  /* The main transaction must be initialized only after the Connection has
   * connected to the database: */
  assert (con->main_trans == NULL);
  {
    #ifndef NDEBUG
      /* Unlike creating a normal Transaction object on a CConnection, creating
       * the main_transaction should not increase the CConnection's reference
       * count.  The CConnection owns a reference to its main_transaction, but
       * not to any other subordinate Transaction objects. */
      const Py_ssize_t con_refcount_orig = con->ob_refcnt;
    #endif
    const int create_main_trans_status = Connection_create_main_trans(con);
    assert (con->ob_refcnt == con_refcount_orig);
    if (create_main_trans_status != 0) { goto fail; }
    assert (con->main_trans != NULL);
  }

  assert (!PyErr_Occurred());
  assert (con != NULL);
  return (PyObject *) con;

  fail:
    assert (PyErr_Occurred());
    #ifdef ENABLE_CONNECTION_TIMEOUT
      if (timeout_params != NULL) {
        if (ConnectionTimeoutParams_destroy(&timeout_params) == 0) {
          assert (timeout_params == NULL);
        }
      }
    #endif
    Py_XDECREF((PyObject *) con);
    return NULL;
} /* pyob_Connection_connect */

static boolean Connection_has_any_open_transaction(CConnection *con) {
  TransactionTracker *trans_node = con->transactions;
  while (trans_node != NULL) {
    Transaction *trans = trans_node->contained;
    assert (trans != NULL);

    if (Transaction_is_active(trans)) { return TRUE; }

    trans_node = trans_node->next;
  }
  return FALSE;
} /* Connection_has_any_open_transaction */

static PyObject *pyob_Connection_closed_get(PyObject *self, PyObject *args) {
  CConnection *con;
  boolean closed;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  #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);
    }
  #endif /* ENABLE_CONNECTION_TIMEOUT */
    closed = (boolean) Connection_is_closed(con);
  #ifdef ENABLE_CONNECTION_TIMEOUT
    if (needed_to_acquire_tp) {
      RELEASE_CON_TP(con);
    }
  }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  return PyBool_FromLong(closed);
} /* Connection_dialect_get */

#ifdef INTERBASE_6_OR_LATER
static PyObject *pyob_Connection_dialect_get(PyObject *self, PyObject *args) {
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  CONN_REQUIRE_OPEN(con);

  return PyInt_FromLong(con->dialect);
} /* Connection_dialect_get */

static PyObject *pyob_Connection_dialect_set(PyObject *self, PyObject *args) {
  CConnection *con;
  unsigned short dialect;

  if (!PyArg_ParseTuple(args, "O!H", &ConnectionType, &con, &dialect)) {
    return NULL;
  }

  CONN_REQUIRE_OPEN(con);

  con->dialect = dialect;

  RETURN_PY_NONE;
} /* Connection_dialect_set */
#endif /* INTERBASE_6_OR_LATER */

static PyObject *pyob_Connection_main_trans_get(PyObject *self, PyObject *args)
{
  PyObject *py_res;
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }
  CON_ACTIVATE(con, return NULL);

  if (con->main_trans == NULL) {
    py_res = Py_None;
  } else {
    py_res = (PyObject *) con->main_trans;
  }
  Py_INCREF(py_res);

  CON_PASSIVATE(con);
  CON_MUST_NOT_BE_ACTIVE(con);
  return py_res;
} /* pyob_Connection_main_trans_get */

static PyObject *pyob_Connection_x_info(
    boolean for_isc_database_info, isc_tr_handle *trans_handle_p,
    PyObject *self, PyObject *args
  )
{
  /* If for_isc_database_info is false, then we're issuing a request to
   * isc_transaction_info. */
  CConnection *con;
  const char *err_preamble =
    for_isc_database_info ? "isc_database_info: " : "isc_transaction_info: ";

  char req_buf[] = {isc_info_end, isc_info_end};
  char res_type; /* 'i'-short integer, 's'-string */

  char *res_buf = NULL;
  size_t res_buf_size = 0;
  size_t res_len = 0;
  size_t i = 0;

  PyObject *py_res = NULL;

  if (!PyArg_ParseTuple(args, "O!bc",
         &ConnectionType, &con, &req_buf[0], &res_type
       )
     )
  { return NULL; }

  CON_ACTIVATE(con, return NULL);

  if (trans_handle_p == NULL && !for_isc_database_info) {
    /* Retrieve the transaction handle pointer from con->main_trans: */
    assert (con->main_trans != NULL);
    trans_handle_p = Transaction_get_handle_p(con->main_trans);
    assert (trans_handle_p != NULL);
  }

  res_buf_size = 64;
  for (;;) {
    assert (res_buf_size <= SHRT_MAX);
    {
      char *res_buf_bigger = kimem_main_realloc(res_buf, res_buf_size);
      if (res_buf_bigger == NULL) {
        /* res_buf might still be non-NULL, but the cleanup block will take
         * care of that. */
        goto fail;
      }
      res_buf = res_buf_bigger;
    }

    memset(res_buf, '\0', res_buf_size);

    ENTER_GDAL
    if (for_isc_database_info) {
      isc_database_info(con->status_vector, &con->db_handle,
          sizeof(req_buf), req_buf,
          /* The cast below is safe because we've validated res_buf_size: */
          (short) res_buf_size, res_buf
        );
    } else {
      isc_transaction_info(con->status_vector, trans_handle_p,
          sizeof(req_buf), req_buf,
          /* The cast below is safe because we've validated res_buf_size: */
          (short) res_buf_size, res_buf
        );
    }
    LEAVE_GDAL
    if (DB_API_ERROR(con->status_vector)) {
      raise_sql_exception(OperationalError, err_preamble, con->status_vector);
      goto fail;
    }

    /* Some info codes, such as isc_info_user_names, don't represent the length
     * of their results in a standard manner, so we scan backward from the end
     * of the result buffer to find where the results end. */
    for (i = res_buf_size - 1; i >= 0; --i) {
      if (res_buf[i] != '\0') {
        break;
      }
    }

    if (res_buf[i] != isc_info_truncated) {
      /* Success; leave the for(ever) loop. */
      break;
    } else {
      /* The previous result buffer wasn't large enough; enlarge it and try
       * again.  The size mustn't exceed SHRT_MAX because the size parameters
       * to isc_database_info are of type short. */
      if (res_buf_size == SHRT_MAX) {
        raise_exception(OperationalError, "Result is too large to fit into"
            " buffer of size SHRT_MAX, yet underlying info function only"
            " accepts buffers with size <= SHRT_MAX."
          );
        goto fail;
      }

      res_buf_size *= 2;
      if (res_buf_size > SHRT_MAX) {
        res_buf_size = SHRT_MAX;
      }
    }
  }

  if (res_buf[i] != isc_info_end) {
    raise_exception(InternalError, "Exited request loop successfully, but"
        " res_buf[i] != isc_info_end."
      );
    goto fail;
  }
  if (res_buf[0] != req_buf[0]) {
    raise_exception(InternalError, "Result code does not match request code.");
    goto fail;
  }

  res_len = i;

  switch (res_type) {
    case 'i':
    case 'I':
      {
        LONG_LONG int_value;
        /* The cluster length will differ depending on whether the integer
         * result is returned as a byte, a short, or an int. */
        short cluster_len;

        ENTER_GDAL
        cluster_len = (short) isc_vax_integer(res_buf + 1, sizeof(short));
        int_value = isc_portable_integer(
            #ifndef INTERBASE_7_OR_LATER
              (unsigned char *)
            #endif
            (res_buf + 1 + sizeof(short)),
            cluster_len
          );
        LEAVE_GDAL

        py_res = PythonIntOrLongFrom64BitValue(int_value);
      }
      break;

    case 's':
    case 'S':
      /* Some info codes, such as isc_info_user_names, don't represent the
       * length of their results in a manner compatible with the previous
       * parsing logic, so we need to return the entire buffer to the Python
       * level. */
      py_res = PyString_FromStringAndSize(res_buf,
          SIZE_T_TO_PYTHON_SIZE(res_len)
        );
      break;

    default:
      raise_exception(InterfaceError, "Unknown result type requested (must be"
          " 'i' or 's')."
        );
      goto fail;
  }

  assert (py_res != NULL);
  assert (!PyErr_Occurred());
  goto clean;
  fail:
    assert (PyErr_Occurred());
    if (py_res != NULL) {
      Py_DECREF(py_res);
      py_res = NULL;
    }
    /* Fall through to clean: */
  clean:
    if (res_buf != NULL) {
      kimem_main_free(res_buf);
    }

    CON_PASSIVATE(con);
    CON_MUST_NOT_BE_ACTIVE(con);

    return py_res;
} /* pyob_Connection_x_info */

static PyObject *pyob_Connection_transactions_get(
    PyObject *self, PyObject *args
  )
{
  PyObject *py_res;
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }
  CON_ACTIVATE(con, return NULL);

  py_res = pyob_TrackerToList((AnyTracker *) con->transactions);

  CON_PASSIVATE(con);
  CON_MUST_NOT_BE_ACTIVE(con);
  return py_res;
} /* pyob_Connection_transactions_get */

static PyObject *pyob_Connection_has_active_transaction(
    PyObject *self, PyObject *args
  )
{
  /* Notice the lack of CON_ACTIVATE/CON_PASSIVATE in this method.  That's
   * necessary because this method is often called as a "probe" when the
   * Connection has non-transparently timed out. */
  boolean res = FALSE;
  CConnection *con;

  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  #ifdef ENABLE_CONNECTION_TIMEOUT
  ACQUIRE_CON_TP_WITH_GIL_HELD(con);
  #endif
  {
    TransactionTracker *trans_node = con->transactions;
    while (trans_node != NULL) {
      Transaction *trans = trans_node->contained;
      assert (trans != NULL);

      if (Transaction_is_active(trans)) {
        res = TRUE;
        break;
      }

      trans_node = trans_node->next;
    }
  }
  #ifdef ENABLE_CONNECTION_TIMEOUT
  RELEASE_CON_TP(con);
  #endif

  return PyBool_FromLong(res);
} /* pyob_Connection_has_active_transaction */

static PyObject *pyob_Connection_database_info(PyObject *self, PyObject *args)
{
  return pyob_Connection_x_info(TRUE, NULL, self, args);
}

static PyObject *pyob_Connection_transaction_info(
    PyObject *self, PyObject *args
  )
{
  return pyob_Connection_x_info(FALSE, NULL, self, args);
}

static PyObject *pyob_Connection_python_wrapper_obj_set(
    PyObject *self, PyObject *args
  )
{
  CConnection *con;
  PyObject *python_wrapper_obj;

  if (!PyArg_ParseTuple(args, "O!O",
         &ConnectionType, &con, &python_wrapper_obj
       )
     ) { goto fail; }

  /* This function is called only by internal code, and should only be called
   * once. */
  if (con->python_wrapper_obj != NULL) {
    raise_exception(InternalError, "Attempt to set Python wrapper object"
        " reference when it had already been set."
      );
    goto fail;
  }
  if (con->main_trans != NULL) {
    raise_exception(InternalError, "Deferred assignment of a CConnection's"
        " python_wrapper_obj member is only supposed to be applied to"
        " CConnections that also deferred the creation of their main"
        " transaction."
      );
    goto fail;
  }

  /* Yes, it is correct not to INCREF this PyObject * before storing it--see
   * comment about the CConnection.python_wrapper_obj field in
   * _kinterbasdb.h */
  assert (python_wrapper_obj != NULL);
  con->python_wrapper_obj = python_wrapper_obj;

  if (Connection_create_main_trans(con) != 0) { goto fail; }
  assert (con->main_trans != NULL);

  RETURN_PY_NONE;

  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* pyob_Connection_python_wrapper_obj_set */

static PyObject *pyob_Connection_timeout_enabled(
    PyObject *self, PyObject *args
  )
{
  CConnection *con;
  if (!PyArg_ParseTuple(args, "O!", &ConnectionType, &con)) { return NULL; }

  #ifdef ENABLE_CONNECTION_TIMEOUT
    return PyBool_FromLong(Connection_timeout_enabled(con));
  #else
    Py_INCREF(Py_False);
    return Py_False;
  #endif
} /* pyob_Connection_timeout_enabled */
