/* KInterbasDB Python Package - Implementation of Streaming Blob Conversion
 *
 * 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>
 */

/* This source file is designed to be directly included in _kiconversion.c,
 * without the involvement of a header file. */

static int BlobReader_close_without_unlink(BlobReader *, boolean);

#ifdef ENABLE_CONNECTION_TIMEOUT
  #define BLOBREADER_REQUIRE_OPEN(self) \
    if (!BlobReader_is_open(self)) { \
      if (BlobReader_con_timed_out(self)) { \
        raise_exception(ConnectionTimedOut, "This BlobReader's Connection" \
            " timed out; the BlobReader can no longer be used." \
          ); \
      } else { \
        raise_exception(ProgrammingError, \
            "I/O operation on closed BlobReader" \
          ); \
      } \
      return NULL; \
    }
#else
  #define BLOBREADER_REQUIRE_OPEN(self) \
    if (!BlobReader_is_open(self)) { \
      raise_exception(ProgrammingError, \
          "I/O operation on closed BlobReader" \
        ); \
      return NULL; \
    }
#endif

#define BLOBREADER_BYTES_AVAILABLE(self) ((self)->total_size - (self)->pos)

#define DECLARE_AND_INITIALIZE_TRANS(self) \
  Transaction *trans; \
  \
  assert (self != NULL); \
  \
  trans = (self)->trans; \
  assert (trans != NULL); \
  assert (Transaction_is_not_closed(trans));

/* Retarded C rule against introducing new declarations except at the beginning
 * of a scope forces us to repeat code here: */
#define DECLARE_AND_INITIALIZE_TRANS_AND_SV(self) \
  Transaction *trans; \
  ISC_STATUS *sv; \
  \
  assert (self != NULL); \
  \
  trans = (self)->trans; \
  assert (trans != NULL); \
  assert (Transaction_is_not_closed(trans)); \
  sv = Transaction_get_sv(trans); \
  assert (sv != NULL);

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

LIFO_LINKED_LIST_DEFINE_BASIC_METHODS_PYALLOC_NOQUAL(
    BlobReaderTracker, BlobReader
  )
/*** BlobReaderTracker MEMBER FUNC DEFINITIONS AND SUPPORTING FUNCS: END *****/

/******************** BlobReader METHODS:BEGIN ********************/
static void BlobReader_create_references_to_superiors(BlobReader *self,
    Transaction *trans
  )
{
  assert (self != NULL);
  assert (self->trans == NULL);
  assert (self->con_python_wrapper == NULL);

  assert (trans != NULL);
  Py_INCREF(trans);
  self->trans = trans;

  /* TAG:TRANSACTION_SUBORDINATE_OBJECT_SURVIVAL_BYPASS: */
  /* If (Transaction_is_main(self->trans)), the Transaction will not own a
   * reference to its connection, so we always create an artificial reference,
   * to ensure that the connection will remain alive at least as long as this
   * BlobReader does. */
  self->con_python_wrapper = Transaction_get_con_python_wrapper(trans);
  assert (self->con_python_wrapper != NULL);
  Py_INCREF(self->con_python_wrapper);
} /* BlobReader_set_superior_references */

static void BlobReader_clear_references_to_superiors(BlobReader *self) {
  assert (self != NULL);
  assert (self->trans != NULL);
  assert (self->con_python_wrapper != NULL);

  /* TAG:TRANSACTION_SUBORDINATE_OBJECT_SURVIVAL_BYPASS: */
  Py_DECREF(self->trans);
  self->trans = NULL;

  Py_DECREF(self->con_python_wrapper);
  self->con_python_wrapper = NULL;
} /* BlobReader_clear_references_to_superiors */

/* This constructor is not accessible to the Python client programmer (i.e.,
 * it's not registered in BlobReaderType's tp_init slot or otherwise exposed)
 * because there's no reason to instantiate BlobReader objects from Python.
 * An attempt to do so raises a TypeError. */
static BlobReader *BlobReader_create(Transaction *trans) {
  BlobReader *self = PyObject_New(BlobReader, &BlobReaderType);
  if (self == NULL) { goto fail; }

  self->state = BLOBREADER_STATE_LIMBO;

  /* blob_handle is assigned a meaningful value by BlobReader_open. */
  self->blob_handle = NULL_BLOB_HANDLE;

  self->total_size = -1;
  self->max_segment_size = 0;
  self->pos = -1;
  self->iter_chunk_size = DTT_BLOB_CHUNK_SIZE_UNSPECIFIED;

  self->trans = NULL; /* Initial nullification of mem. */
  self->con_python_wrapper = NULL;
  BlobReader_create_references_to_superiors(self, trans);
  assert (self->trans != NULL);
  assert (self->con_python_wrapper != NULL);

  return self;
  fail:
    Py_XDECREF(self);
    return NULL;
} /* BlobReader_create */

static int BlobReader_open(BlobReader *self, ISC_QUAD *blob_id) {
  /* Returns 0 if the op succeeds; -1 on error. */
  DECLARE_AND_INITIALIZE_TRANS_AND_SV(self)
  assert ((self)->blob_handle == NULL_BLOB_HANDLE);

  /* Based on the ID of the blob, open a handle to it.  The handle will be used
   * in all future operations on the blob. */
  {
    /* Notice that we call Transaction_get_handle_p while holding the GIL. */
    isc_db_handle *db_handle_p = Transaction_get_db_handle_p(trans);
    isc_tr_handle *tr_handle_p = Transaction_get_handle_p(trans);

    ENTER_GDAL
    isc_open_blob2(sv, db_handle_p, tr_handle_p,
        &self->blob_handle, blob_id,
        /* Last two params indicate "no blob parameter buffer supplied": */
        0, NULL
      );
    LEAVE_GDAL
  }
  if (DB_API_ERROR(sv)) {
    raise_sql_exception(OperationalError, "BlobReader_open.isc_open_blob2: ",
        sv
      );
    return -1;
  }

  /* Determine the total size and max segment size of the blob. */
  if (0 !=
      _blob_info_total_size_and_max_segment_size(sv,
          &self->blob_handle, &self->total_size, &self->max_segment_size
        )
    )
  { return -1; }
  assert (self->total_size >= 0);
  /* max_segment_size is guaranteed to be >= 0 because it's unsigned. */

  if (BlobReaderTracker_add(&trans->open_blobreaders, self) == -1) {
    return -1;
  }

  self->pos = 0;
  self->state = BLOBREADER_STATE_OPEN;
  return 0;
} /* BlobReader_open */

static int _BlobReader_close_handle_only(
    BlobReader *self, boolean allowed_to_raise
  )
{
  /* Returns 0 if the op succeeds; -1 on error. */
  int status = -1;

  DECLARE_AND_INITIALIZE_TRANS_AND_SV(self)
  assert (self->blob_handle != NULL_BLOB_HANDLE);

  {
    /* 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

    isc_close_blob(sv, &self->blob_handle);

    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(sv)) {
    assert (self->blob_handle == NULL_BLOB_HANDLE);
    status = 0;
  } else {
    if (allowed_to_raise) {
      raise_sql_exception(OperationalError, "_BlobReader_close: ", sv);
    } else {
      self->blob_handle = NULL_BLOB_HANDLE;
    }
  }

  return status;
} /* _BlobReader_close_handle_only */

static int _BlobReader_close(BlobReader *self,
    boolean should_unlink_self, boolean allowed_to_raise
  )
{
  /* Returns 0 if the op succeeds; -1 on error. */
  int status = 0;

  DECLARE_AND_INITIALIZE_TRANS(self)

  assert (BlobReader_is_open(self));
  #ifdef ENABLE_CONNECTION_TIMEOUT
    assert (
          Connection_timeout_enabled(Transaction_get_con(trans))
        ? CURRENT_THREAD_OWNS_CON_TP(Transaction_get_con(trans))
        : TRUE
      );
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  if (_BlobReader_close_handle_only(self, allowed_to_raise) != 0) {
    /* If we're supposed to do our best to close regardless of any problems
     * encountered, forge ahead despite the error closing the blob handle. */
    status = -1;
    if (allowed_to_raise) {
      goto fail;
    } else {
      self->blob_handle = NULL_BLOB_HANDLE;
      SUPPRESS_EXCEPTION;
    }
  }
  assert (self->blob_handle == NULL_BLOB_HANDLE);

  if (should_unlink_self) {
    if (BlobReaderTracker_remove(&trans->open_blobreaders, self, TRUE) != 0) {
      status = -1;
      if (allowed_to_raise) {
        goto fail;
      } else {
        SUPPRESS_EXCEPTION;
      }
    }
  }

  self->pos = -1;
  self->state = BLOBREADER_STATE_CLOSED;

  goto clean;
  fail:
    assert (allowed_to_raise ? !!PyErr_Occurred() : !PyErr_Occurred());
    /* Fall through to clean: */
  clean:
    assert (!allowed_to_raise ? self->state == BLOBREADER_STATE_CLOSED : TRUE);
    return status;
} /* _BlobReader_close */

static int BlobReader_close_without_unlink(
    BlobReader *self, boolean allowed_to_raise
  )
{
  return _BlobReader_close(self, FALSE, allowed_to_raise);
} /* BlobReader_close_without_unlink */

static int BlobReader_untrack(BlobReader *self, boolean allowed_to_raise) {
  int res = -1;
  assert (self->trans != NULL);
  assert (self->con_python_wrapper != NULL);

  assert (BlobReader_is_open(self));

  if (BlobReader_close_without_unlink(self, allowed_to_raise) == 0) {
    res = 0;
  }

  /* 2007.01.17: Closing a BlobReader no longer clears its superior refs: */
  assert (self->trans != NULL);
  assert (self->con_python_wrapper != NULL);
  assert (!allowed_to_raise ? self->state != BLOBREADER_STATE_OPEN : TRUE);
  return res;
} /* BlobReader_close_untrack */

static int BlobReader_close_with_unlink(
    BlobReader *self, boolean allowed_to_raise
  )
{
  #ifdef ENABLE_CONNECTION_TIMEOUT
    assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);
  #endif /* ENABLE_CONNECTION_TIMEOUT */
  return _BlobReader_close(self, TRUE, allowed_to_raise);
} /* BlobReader_close_with_unlink */

static PyObject *pyob_BlobReader_close(BlobReader *self) {
  /* Thin Python wrapper over BlobReader_close_with_unlink. */
  PyObject *ret = NULL;
  CConnection *con = NULL;

  #ifdef ENABLE_CONNECTION_TIMEOUT
    boolean con_timeout_was_enabled;
    boolean con_activation_succeeded = TRUE;
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  BLOBREADER_REQUIRE_OPEN(self);

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

  #ifdef ENABLE_CONNECTION_TIMEOUT
    {
      con_timeout_was_enabled = (boolean) Connection_timeout_enabled(con);

      if (con_timeout_was_enabled) {
        assert (!CURRENT_THREAD_OWNS_CON_TP(con));
        ACQUIRE_CON_TP_WITH_GIL_HELD(con);
        CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION__ALREADY_LOCKED(
            con, goto fail_without_passivating
          );
        assert (CURRENT_THREAD_OWNS_CON_TP(con));
      }
    }
  #endif /* ENABLE_CONNECTION_TIMEOUT */

  if (BlobReader_close_with_unlink(self, TRUE) == 0) {
    assert (!BlobReader_is_open(self));
    ret = Py_None;
    Py_INCREF(Py_None);
  } else {
    goto fail;
  }
  /* 2007.01.17: Closing a BlobReader no longer clears its superior refs: */
  assert (self->trans != NULL);
  assert (self->con_python_wrapper != NULL);

  goto clean;
  #ifdef ENABLE_CONNECTION_TIMEOUT
    fail_without_passivating:
      con_activation_succeeded = FALSE;
      /* Fall through to fail: */
  #endif /* ENABLE_CONNECTION_TIMEOUT */
  fail:
    assert (PyErr_Occurred());
    assert (ret == NULL);
    assert (con_activation_succeeded);
    /* Fall through to clean: */
  clean:
    #ifdef ENABLE_CONNECTION_TIMEOUT
      if (con_timeout_was_enabled) {
        if (con_activation_succeeded) {
          assert (CURRENT_THREAD_OWNS_CON_TP(con));
          CON_PASSIVATE__ALREADY_LOCKED(con);
        }
        TP_UNLOCK(con->timeout);
        assert (!CURRENT_THREAD_OWNS_CON_TP(con));
      }
    #endif /* ENABLE_CONNECTION_TIMEOUT */
    CON_MUST_NOT_BE_ACTIVE(con);

    return ret;
} /* pyob_BlobReader_close */

static PyObject *BlobReader_read(BlobReader *self, int req_chunk_size) {
  const int bytes_available = BLOBREADER_BYTES_AVAILABLE(self);

  /* The caller should've already ensured that self is open: */
  assert (BlobReader_is_open(self));
  assert (self->trans != NULL);
  CON_MUST_ALREADY_BE_ACTIVE(Transaction_get_con(self->trans));

  /* If the requested number of bytes was negative (including the default value
   * of -1), read all bytes available.
   * If the requested number of bytes exceeds the available number, reduce the
   * requested number. */
  if (req_chunk_size < 0 || req_chunk_size > bytes_available) {
    req_chunk_size = bytes_available;
  }

  if (req_chunk_size == 0) {
    /* No data left; match Python file-like interface by returning the empty
     * string. */
    return PyString_FromStringAndSize("", 0);
  } else {
    /* The documentation for isc_get_segment says:
     *   "isc_segment indicates the buffer is not large enough to hold the
     *    entire current segment; the next call to isc_get_segment() gets the
     *    next chunk of the oversized segment rather than getting the next
     *    segment."
     * conv_out_blob_materialized_in_single_chunk exploits this behavior to
     * return exactly req_chunk_size bytes, at which point the generated Python
     * string is filled and isc_get_segment raises the isc_segment code.  Any
     * subsequent call to isc_get_segment will start where the previous one
     * left off, which is exactly what we need. */
    PyObject *py_str = conv_out_blob_materialized_in_single_chunk(
        Transaction_get_sv(self->trans), &self->blob_handle,
        self->max_segment_size, req_chunk_size, TRUE
      );
    if (py_str == NULL) { return NULL; }
    self->pos += req_chunk_size;

    return py_str;
  }
} /* BlobReader_read */

static PyObject *pyob_BlobReader_read(BlobReader *self, PyObject *args) {
  PyObject *ret = NULL;
  int req_chunk_size = -1;

  BLOBREADER_REQUIRE_OPEN(self);
  assert (self->trans != NULL);

  CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(
      Transaction_get_con(self->trans), return NULL
    );

  if (!PyArg_ParseTuple(args, "|i", &req_chunk_size)) { goto fail; }

  ret = BlobReader_read(self, req_chunk_size);
  if (ret == NULL) { goto fail; }

  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (ret == NULL);
    /* Fall through to clean: */
  clean:
    {
      CConnection *con = Transaction_get_con(self->trans);
      CON_PASSIVATE(con);
      CON_MUST_NOT_BE_ACTIVE(con);
    }
    return ret;
} /* pyob_BlobReader_read */

static PyObject *pyob_BlobReader__iter_read_chunk(BlobReader *self) {
  /* Supporting method for pyob_BlobReader_chunks (this method is called to
   * return each chunk). */
  PyObject *ret = NULL;

  BLOBREADER_REQUIRE_OPEN(self);
  assert (self->trans != NULL);

  CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(
      Transaction_get_con(self->trans), return NULL
    );

  if (self->iter_chunk_size == DTT_BLOB_CHUNK_SIZE_UNSPECIFIED) {
    raise_exception(ProgrammingError, "This method is private and must not be"
        " called directly."
      );
    goto fail;
  }

  ret = BlobReader_read(self, self->iter_chunk_size);
  if (ret == NULL) { goto fail; }

  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (ret == NULL);
    /* Fall through to clean: */
  clean:
    {
      CConnection *con = Transaction_get_con(self->trans);
      CON_PASSIVATE(con);
      CON_MUST_NOT_BE_ACTIVE(con);
    }
    return ret;
} /* pyob_BlobReader__iter_read_chunk */

static PyObject *pyob_BlobReader_chunks(BlobReader *self, PyObject *args) {
  /* This method is equivalent to the following Python method definition:
   *   def chunks(self, chunk_size):
   *       return iter(lambda: self.read(chunk_size), '')                    */
  PyObject *it = NULL;
  int req_chunk_size = -1;

  BLOBREADER_REQUIRE_OPEN(self);
  assert (self->trans != NULL);

  CON_ACTIVATE__FORBID_TRANSPARENT_RESUMPTION(
      Transaction_get_con(self->trans), return NULL
    );

  if (self->iter_chunk_size != DTT_BLOB_CHUNK_SIZE_UNSPECIFIED) {
    raise_exception(ProgrammingError, "At most one iterator can be opened on"
        " a given BlobReader via the chunks() method."
      );
    goto fail;
  }

  if (!PyArg_ParseTuple(args, "i", &req_chunk_size)) { goto fail; }
  if (req_chunk_size <= 0) {
    raise_exception(ProgrammingError, "chunk size must be > 0");
    goto fail;
  }
  self->iter_chunk_size = req_chunk_size;

  {
    PyObject *bound_method__iter_read_chunk;
    PyObject *sentinel;

    bound_method__iter_read_chunk = PyObject_GetAttr(
        (PyObject *) self, blob_streaming__method_name__iter_read_chunk
      );
    if (bound_method__iter_read_chunk == NULL) { goto fail; }

    sentinel = PyString_FromStringAndSize("", 0);
    if (sentinel == NULL) {
      Py_DECREF(bound_method__iter_read_chunk);
      goto fail;
    }

    it = PyCallIter_New(bound_method__iter_read_chunk, sentinel);

    Py_DECREF(bound_method__iter_read_chunk);
    Py_DECREF(sentinel);
  }

  goto clean;
  fail:
    assert (PyErr_Occurred());
    assert (it == NULL);
    /* Fall through to clean: */
  clean:
    {
      CConnection *con = Transaction_get_con(self->trans);
      CON_PASSIVATE(con);
      CON_MUST_NOT_BE_ACTIVE(con);
    }
    return it;
} /* pyob_BlobReader_chunks */

static PyObject *pyob_BlobReader_tell(BlobReader *self) {
  BLOBREADER_REQUIRE_OPEN(self);
  /* self->pos is of type ISC_LONG, which is always 32-bit, even on 64-bit
   * platforms, so using PyInt_FromLong is fine: */
  return PyInt_FromLong(self->pos);
} /* pyob_BlobReader_tell */

static PyObject *pyob_BlobReader_mode_get(BlobReader *self, void *closure) {
  /* BlobReaders only support one mode ("rb"), so we just return a new
   * reference to a perpetually cached "constant" Python string with that
   * value. */
  Py_INCREF(blob_streaming__reader_mode__rb);
  return blob_streaming__reader_mode__rb;
} /* pyob_BlobReader_mode_get */

static PyObject *pyob_BlobReader_closed_get(BlobReader *self, void *closure) {
  /* Although a BlobReader has three states, if we have to choose a boolean,
   * it's more appropriate to consider the "limbo" state closed than open. */
  return PyBool_FromLong(!BlobReader_is_open(self));
} /* pyob_BlobReader_closed_get */

static PyObject *pyob_BlobReader_repr(BlobReader *self) {
  if (BlobReader_is_open(self)) {
    return PyString_FromFormat("<%s at %p (open; %ld of %ld bytes read)>",
        self->ob_type->tp_name, (void *) self,
        /* Up-cast in anticipation of possible enlargement of ISC_LONG: */
        (long) self->pos, (long) self->total_size
      );
  } else {
    return PyString_FromFormat("<%s at %p (closed)>",
        self->ob_type->tp_name, (void *) self
      );
  }
} /* pyob_BlobReader_repr */

static void pyob_BlobReader___del__(BlobReader *self) {
  assert (NOT_RUNNING_IN_CONNECTION_TIMEOUT_THREAD);

  if (self->trans != NULL) {
    /* TAG:TRANSACTION_SUBORDINATE_OBJECT_SURVIVAL_BYPASS: */
    /* We need to make sure that the Transaction and the connection that
     * underlies it stay alive at least until we've completely the process of
     * removing their references to self.
     *
     * Fortunately, as of the changes on 2007.01.17, this is easy:
     * BlobReader_create_references_to_superiors creates references to *both*
     * the Transaction and the kinterbasdb.Connection that underlies it.
     * BlobReader_create_references_to_superiors creates that artificial
     * reference to the kinterbasdb.Connection in case
     *   Transaction_is_main(self->trans)
     * , which would mean that self->trans doesn't actually hold a physical
     * reference to its connection.
     *
     * BlobReader_clear_references_to_superiors, which is not invoked until the
     * end of this very code block, clears both of those references in the
     * appropriate order.
     *
     * So, we do not need to do anything special to ensure that the Transaction
     * survives long enough for self to be torn down in an orderly manner. */
    CConnection *con;
    Transaction *trans = self->trans;

    assert (trans->ob_refcnt >= 1);

    assert (self->con_python_wrapper != NULL);
    /* We can make the following assertion about the kinterbasdb.Connection
     * instance's reference count because we know that self owns a reference
     * *directly* to the kinterbasdb.Connectioin, regardless of whether
     * self->trans actually owns a physical reference to it (see comment above
     * for details): */
    assert (self->con_python_wrapper->ob_refcnt >= 1);

    con = Transaction_get_con(trans);
    assert (con == NULL ? !BlobReader_is_open(self) : TRUE);
    if (con != NULL) {
      #ifdef ENABLE_CONNECTION_TIMEOUT
        assert (
              Connection_timeout_enabled(con)
            ? !CURRENT_THREAD_OWNS_CON_TP(con)
            : TRUE
          );
        ACQUIRE_CON_TP_WITH_GIL_HELD(con);
        assert (
              Connection_timeout_enabled(con)
            ? CURRENT_THREAD_OWNS_CON_TP(con)
            : TRUE
          );
      #endif /* ENABLE_CONNECTION_TIMEOUT */

      if (BlobReader_is_open(self)) {
        BlobReader_close_with_unlink(self, FALSE);
      }
      /* As of 2007.01.17, BlobReader_close_with_unlink should no longer clear
       * self->trans: */
      assert (self->trans != NULL);
      assert (self->trans == trans);
      assert (trans->ob_refcnt >= 1);
      assert (self->con_python_wrapper != NULL);

      #ifdef ENABLE_CONNECTION_TIMEOUT
        assert (
              Connection_timeout_enabled(con)
            ? CURRENT_THREAD_OWNS_CON_TP(con)
            : TRUE
          );
        RELEASE_CON_TP(con);
        assert (
              Connection_timeout_enabled(con)
            ? !CURRENT_THREAD_OWNS_CON_TP(con)
            : TRUE
          );
      #endif /* ENABLE_CONNECTION_TIMEOUT */
    } /* end of if (con != NULL) block */

    BlobReader_clear_references_to_superiors(self);
    assert (self->trans == NULL);
    assert (self->con_python_wrapper == NULL);
    /* Note that if more code is ever added after this point, IT IS NO LONGER
     * SAFE TO REFER TO trans OR con, so code such as this should be enabled:
     *   trans = NULL;
     *   con = NULL;   */
  } /* end of if (self->trans != NULL) block */

  assert (!BlobReader_is_open(self));
  assert (self->trans == NULL);
  assert (self->con_python_wrapper == NULL);
  assert (self->blob_handle == NULL_BLOB_HANDLE);

  PyObject_Del(self);
} /* pyob_BlobReader___del__ */

static PyMethodDef BlobReader_methods[] = {
    {"close", (PyCFunction) pyob_BlobReader_close,
        METH_NOARGS,
        "'close' method of file-like interface"
      },
    {"read", (PyCFunction) pyob_BlobReader_read,
        METH_VARARGS,
        "'read' method of file-like interface"
      },
    {"chunks", (PyCFunction) pyob_BlobReader_chunks,
        METH_VARARGS,
        "Return an iterator over M chunks of the indicated size and N of a"
        " potentially smaller size.  Both M and N can be 0; N is never greater"
        " than 1.  This method can only be called once on a given BlobReader."
      },
    /* Private support method for the chunks method: */
    {"_iter_read_chunk", (PyCFunction) pyob_BlobReader__iter_read_chunk,
        METH_NOARGS,
        NULL
      },
    {"tell", (PyCFunction) pyob_BlobReader_tell, METH_NOARGS,
        "'tell' method of file-like interface"
      },
    {NULL}  /* sentinel */
  };


static PyGetSetDef BlobReader_getters_setters[] = {
    {"mode", (getter) pyob_BlobReader_mode_get, NULL,
        "'mode' read-only property of file-like interface", NULL
      },
    {"closed", (getter) pyob_BlobReader_closed_get, NULL,
        "'closed' read-only property of file-like interface", NULL
      },
    {NULL}  /* sentinel */
  };

/******************** BlobReader METHODS:END ********************/

/************ CHUNKED BLOB WRITING FUNCTIONS:BEGIN **************/
static InputStatus conv_in_blob_from_pyfilelike(
    PyObject *py_filelike,
    ISC_QUAD *blob_id,
    ISC_STATUS *status_vector, isc_db_handle db_handle,
    isc_tr_handle trans_handle
  )
{
  isc_blob_handle blob_handle = NULL_BLOB_HANDLE;
  isc_blob_handle *blob_handle_ptr = &blob_handle;

  PyObject *read_method;
  PyObject *chunk = NULL;

  assert (py_filelike != NULL);

  /* Retrieve bound method py_filelike.read and call that (potentially numerous
   * times) instead of looking up the method repeatedly. */
  read_method = PyObject_GetAttr(py_filelike,
      blob_streaming__method_name_read
    );
  if (read_method == NULL) { goto fail; }

  /* Create a blob and retrieve its handle into blob_handle. */
  ENTER_GDAL
  isc_create_blob2(status_vector,
      &db_handle, &trans_handle,
      blob_handle_ptr, blob_id,
      /* Last two params indicate "no blob parameter buffer supplied". */
      0, NULL
    );
  LEAVE_GDAL
  if (DB_API_ERROR(status_vector)) {
    raise_sql_exception(OperationalError,
        "conv_in_blob_from_pyfilelike.isc_create_blob2: ", status_vector
      );
    goto fail;
  }

  for (;;) {
    Py_ssize_t chunk_len;
    char *chunk_buf;

    /* Call the previously retrieved bound method with a perpetually cached
     * argument tuple for best performance. */
    chunk = PyObject_CallObject(read_method,
        blob_streaming__1Tuple_containing_MAX_BLOB_SEGMENT_SIZE
      );
    if (chunk == NULL) { goto fail_with_blob_cancel; }
    if (!PyString_CheckExact(chunk)) { /* unicode object is not acceptable. */
      raise_exception(ProgrammingError, "File-like object's read method must"
          " return object of type str (conceptually, a byte buffer)."
        );
      goto fail_with_blob_cancel;
    }

    chunk_len = PyString_GET_SIZE(chunk);
    if (chunk_len == 0) {
      Py_DECREF(chunk); /* the empty string */
      chunk = NULL;
      break; /* File-like object is exhausted. */
    } else if (chunk_len > MAX_BLOB_SEGMENT_SIZE || chunk_len > INT_MAX) {
      PyObject *err_msg;
      if (chunk_len > INT_MAX) {
        err_msg = PyString_FromString("The database API does not yet"
            " officially support blobs larger than 2 GB."
          );
      } else {
        err_msg = PyString_FromFormat("Requested %d bytes from file-like"
            " object's read method; received too many"
            " (" Py_ssize_t_STRING_FORMAT ").",
            (int) MAX_BLOB_SEGMENT_SIZE, chunk_len
          );
      }
      if (err_msg != NULL) {
        raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
        Py_DECREF(err_msg);
      }
      goto fail_with_blob_cancel;
    }

    chunk_buf = PyString_AS_STRING(chunk);

    ENTER_GDAL
    isc_put_segment(status_vector, blob_handle_ptr,
        (unsigned short) chunk_len, chunk_buf
      );
    LEAVE_GDAL
    if (DB_API_ERROR(status_vector)) {
      raise_sql_exception(OperationalError,
          "conv_in_blob_from_pyfilelike.isc_put_segment: ", status_vector
        );
      goto fail_with_blob_cancel;
    }

    Py_DECREF(chunk);
  }

  ENTER_GDAL
  isc_close_blob(status_vector, blob_handle_ptr);
  LEAVE_GDAL
  if (!DB_API_ERROR(status_vector)) {
    blob_handle = NULL_BLOB_HANDLE;
  } else {
    raise_sql_exception(OperationalError,
        "conv_in_blob_from_pyfilelike.isc_close_blob: ", status_vector
      );
    goto fail_with_blob_cancel;
  }


  assert (blob_handle == NULL_BLOB_HANDLE);
  assert (chunk == NULL);
  assert (read_method != NULL);
  Py_DECREF(read_method);

  return INPUT_OK;

  fail_with_blob_cancel:
    /* This function encountered an error while it had an open blob handle, so
     * try to cancel the blob: */
    ENTER_GDAL
    isc_cancel_blob(status_vector, blob_handle_ptr);
    /*if (DB_API_ERROR(status_vector)) {*/
      /* The database client library wasn't even able to do cancel the blob, so
       * just forget about it (don't free, because it's a handle, which is not
       * necessarily a pointer). */
    /*}*/
    blob_handle = NULL_BLOB_HANDLE;
    LEAVE_GDAL
    /* Fall through to regular failure: */

  fail:
    assert (PyErr_Occurred());

    assert (blob_handle == NULL_BLOB_HANDLE);

    Py_XDECREF(read_method);
    Py_XDECREF(chunk);

    return INPUT_ERROR;
} /* conv_in_blob_from_pyfilelike */
/************ CHUNKED BLOB WRITING FUNCTIONS:END **************/
