/* KInterbasDB Python Package - Implementation of Array Conversion (both ways)
 *
 * 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. */

/* Note:  kinterbasdb's array-handling code does not implicitly transform array
 * shape (i.e., pad too-short sequences with NULL), because the database engine
 * does not support NULL array elements (see IB 6 API guide, page 153). */

/* The isc_array_lookup_bounds function, at least in FB 1.5 and 2.0, has a
 * concurrency bug that causes severe problems if the function is called from
 * multiple threads at once.  (In the lookup_desc function in array.epp, a
 * global database handle is set to the one the client code has passed in, and
 * then some GPRE-generated code is run.  Even synchronizing all calls to
 * isc_array_lookup_bounds doesn't fix the problem, because lookup_desc tries
 * to clear open requests its previous handle.)
 *
 * On 2006.01.30, DSR reimplemented the functionality of
 * isc_array_lookup_bounds in kinterbasdb (chiefly in _array_descriptor.py) in
 * order to bypass the concurrency bug.  Defining
 * USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS will restore the old code, which is
 * slower, in addition to being buggy. */
/* #define USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS */

/******************** HARD-CODED LIMITS:BEGIN ********************/

/* MAXIMUM_NUMBER_OF_ARRAY_DIMENSIONS is an IB/Firebird engine constraint, not
 * something we could overcome here in kinterbasdb. */
#define MAXIMUM_NUMBER_OF_ARRAY_DIMENSIONS 16

/******************** HARD-CODED LIMITS:END ********************/

/******************** CONVENIENCE DEFS:BEGIN ********************/

#define ARRAY_ROW_MAJOR 0
#define ARRAY_COLUMN_MAJOR 1

#define DIMENSION_SIZE_END_MARKER -1

/* VARCHAR array elements are stored differently from the way conventional
 * VARCHAR fields are stored.  Instead of 2 bytes at the beginning containing
 * the size of the string value, array-element VARCHARs apparently have 2 null
 * bytes at the end that are not used. */
#define _ADJUST_ELEMENT_SIZE_FOR_VARCHAR_IF_NEC(data_type, size_of_el) \
  if (data_type == blr_varying || data_type == blr_varying2) { \
    size_of_el += 2; \
  }

#define SQLSUBTYPE_DETERMINATION_ERROR -999

/******************** CONVENIENCE DEFS:END ********************/

/******************** FUNCTION PROTOTYPES:BEGIN ********************/

/* Output functions: */
static PyObject *conv_out_array(
    Cursor *cursor, short sqlvar_index,
    ISC_QUAD *array_id,

    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle_p,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  );

static PyObject *conv_out_array_element(
    Cursor *cursor, short sqlvar_index,
    char *data,

    short data_type, size_t size_of_single_element,
    short scale,

    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  );

static PyObject *_extract_db_array_buffer_to_pyseq(
    Cursor *cursor, short sqlvar_index,
    char **data_slot, short *dimension_sizes_ptr,

    /* Boilerplate parameters: */
    short data_type, size_t size_of_single_element,
    short scale,

    ISC_STATUS *status_vector, isc_db_handle *db_handle,
    isc_tr_handle *trans_handle_p,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  );


/* Input functions: */
static InputStatus conv_in_array(
    PyObject *py_input, ISC_QUAD **array_id_slot,
    Cursor *cursor, short sqlvar_index,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  );

static InputStatus conv_in_array_element(
    PyObject *py_input, char **data_slot,

    unsigned short dialect,
    short data_type, short data_subtype,
    size_t size_of_single_element, short scale,
    PyObject *converter,

    ISC_STATUS *status_vector, isc_db_handle *db_handle,
    isc_tr_handle *trans_handle_p,
    Cursor *cur
  );

static InputStatus _extract_pyseq_to_db_array_buffer(
    PyObject *py_seq, short *dimension_sizes_ptr,

    /* Boilerplate parameters: */
    char **data_slot,

    unsigned short dialect,
    short data_type, short data_subtype,
    size_t size_of_single_element, short scale,
    PyObject *converter,

    ISC_STATUS *status_vector, isc_db_handle *db_handle,
    isc_tr_handle *trans_handle_p,
    Cursor *cur
  );


/* Functions common to both input and output: */
static ISC_ARRAY_DESC *_look_up_array_descriptor(
    Transaction *trans,
    /* These strings aren't null-terminated: */
    char *sqlvar_rel_name, short sqlvar_rel_name_length,
    char *sqlvar_field_name, short sqlvar_field_name_length
  );

static short *_extract_dimensions_sizes(
    ISC_ARRAY_DESC *desc,
    /* output param: */
    int *total_number_of_elements
  );

static short _determine_sqlsubtype_for_array(
    Transaction *trans,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  );

/******************** FUNCTION PROTOTYPES:END ********************/

/******************** INPUT FUNCTIONS:BEGIN ********************/

static InputStatus conv_in_array(
    PyObject *py_input,
    ISC_QUAD **array_id_slot,
    Cursor *cursor, short sqlvar_index,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  )
{
  InputStatus status = INPUT_OK;
  ISC_ARRAY_DESC *desc;
  short *dimensions = NULL;
  unsigned short number_of_dimensions;
  int total_number_of_elements = 0;

  short data_type = -1;
  size_t size_of_single_element;

  char *source_buf = NULL;
  char *source_buf_walker = NULL;
  size_t source_buf_size;

  ISC_STATUS *status_vector = cursor->status_vector;
  isc_db_handle *db_handle = Transaction_get_db_handle_p(cursor->trans);
  isc_tr_handle *trans_handle_p = Transaction_get_handle_p(cursor->trans);

  PyObject *converter = NULL;
  short data_subtype = -1;

  /* Read the database array descriptor for this field. */
  desc = _look_up_array_descriptor(cursor->trans,
      rel_name, rel_name_length,
      field_name, field_name_length
    );
  if (desc == NULL) { goto fail; }

  data_type = desc->array_desc_dtype;
  number_of_dimensions = desc->array_desc_dimensions;

  size_of_single_element = desc->array_desc_length;
  _ADJUST_ELEMENT_SIZE_FOR_VARCHAR_IF_NEC(data_type, size_of_single_element);

  /* Populate the short-array named dimensions.  (The _extract_dimensions_sizes
   * function also sets total_number_of_elements to its appropriate value.) */
  dimensions = _extract_dimensions_sizes(desc, &total_number_of_elements);
  if (dimensions == NULL) { goto fail; }
  /* The database engine doesn't allow zero-element arrays. */
  assert (total_number_of_elements > 0);

  /* Validate the incoming Python sequence to ensure that its shape matches
   * that defined by the database array descriptor for this field.  See comment
   * near top of this file for explanation of why kinterbasdb doesn't
   * automatically None-pad sequences that have too few elements. */
  source_buf_size = size_of_single_element * total_number_of_elements;
  source_buf = kimem_main_malloc(source_buf_size);
  if (source_buf == NULL) { goto fail; }
  source_buf_walker = source_buf;

  assert (data_type != -1);
  data_subtype = _determine_sqlsubtype_for_array(cursor->trans,
      rel_name, rel_name_length, field_name, field_name_length
    );
  if (data_subtype == SQLSUBTYPE_DETERMINATION_ERROR) { goto fail; }
  Transaction_stats_record_ps_executed(cursor->trans);

  { short scale = desc->array_desc_scale;

    /* Find the dynamic type translator (if any) for this array's type.  Note
     * that the translator is applied to individual elements of the array, not
     * to the array as a whole. */
    converter = cursor_get_in_converter(cursor, sqlvar_index,
        data_type, data_subtype, scale, TRUE
      );
    if (converter == NULL) { goto fail; }
    /* At this point, converter is either a Python callable, if there was a
     * registered converter for this array's element type, or Py_None if there
     * was not. */
    assert (source_buf_walker != NULL);
    status = _extract_pyseq_to_db_array_buffer(
        py_input, dimensions,

        /* For conversion: */
        &source_buf_walker,

        Transaction_get_dialect(cursor->trans),
        data_type, data_subtype,
        size_of_single_element, scale,
        converter,

        status_vector, db_handle, trans_handle_p,
        cursor
      );
    if (status != INPUT_OK) { goto fail; }
  }

  /* Successful completion requires the entire buffer to have been filled: */
  assert (((size_t) (source_buf_walker - source_buf)) == source_buf_size);

  /* Call isc_array_put_slice to store the incoming value in the database.
   * A NULL array id tells isc_array_put_slice to "create or replace" the
   * existing array in the database (kinterbasdb does not support modifying
   * segments of an existing array--instead, it replaces the old array value in
   * the database with a new one). */
  assert (*array_id_slot == NULL);
  { ISC_QUAD *array_id;
    array_id = *array_id_slot = kimem_main_malloc(sizeof(ISC_QUAD));
    if (array_id == NULL) { goto fail; }

    /* "Nullify" the array id: */
    /* 2003.01.25:  In FB 1.5a5, isc_quad_high/isc_quad_low no longer work,
     * but gds_quad_high/gds_quad_low work with both 1.0 and 1.5a5. */
    array_id->gds_quad_high = 0;
    array_id->gds_quad_low = 0;

    ENTER_GDAL
    isc_array_put_slice(status_vector,
        db_handle, trans_handle_p,
        array_id, desc, source_buf, (ISC_LONG *) &source_buf_size
      );
    LEAVE_GDAL

    if (DB_API_ERROR(status_vector)) { goto fail_operationerror; }
  }

  /* array_id_slot (a pointer to a pointer passed in by the caller) is freshly
   * initialized by isc_array_put_slice.  In effect, it is "passed back" to the
   * caller, so that it can be accessed in the XSQLVAR when the statement is
   * executed. */

  /* We've stored the array successfully; now clean up. */
  goto cleanup;

  fail_operationerror:
    raise_sql_exception(OperationalError, "Array input conversion: ",
        status_vector
      );
    /* Fall through to fail. */

  fail:
    status = INPUT_ERROR;
    /* Fall through to cleanup. */

  cleanup:
    assert (status == INPUT_OK ?
        PyErr_Occurred() == NULL : PyErr_Occurred() != NULL
      );

    #ifdef USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS
    if (desc != NULL) { kimem_main_free(desc); }
    #endif

    if (dimensions != NULL) { kimem_main_free(dimensions); }
    if (source_buf != NULL) { kimem_main_free(source_buf); }

    if (status != INPUT_OK && *array_id_slot != NULL) {
      kimem_main_free(*array_id_slot);
      *array_id_slot = NULL;
    }

    return status;
} /* conv_in_array */


#define CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(conversion_code) \
  TRY_INPUT_CONVERSION( (conversion_code), fail );

/* A "standard" DB type code is like SQL_LONG rather than blr_long. */
#define CONV_IN_ARRAY_ELEMENT_CONVERT_INTEGER_TYPE(standard_db_type_code) \
  CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION( \
    conv_in_internal_integer_types_array(py_input_converted, data_slot, \
        dialect, standard_db_type_code, data_subtype, scale, cur \
      ) \
    );

static InputStatus conv_in_array_element(
    PyObject *py_input, char **data_slot,

    unsigned short dialect,
    short data_type, short data_subtype,
    size_t size_of_single_element, short scale,
    PyObject *converter,

    ISC_STATUS *status_vector, isc_db_handle *db_handle,
    isc_tr_handle *trans_handle_p,
    Cursor *cur
  )
{
  InputStatus status;
  PyObject *py_input_converted;

  assert (py_input != NULL);
  assert (*data_slot != NULL);

  py_input_converted = dynamically_type_convert_input_obj_if_necessary(
      py_input,
      TRUE, /* it IS an array element */
      dialect,
      data_type, data_subtype, scale,
      converter
    );
  if (py_input_converted == NULL) { goto fail; }

  switch (data_type) {

  case blr_text:
  case blr_text2:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_text_array(data_slot, size_of_single_element, ' ')
      );
    break;

  case blr_varying:
  case blr_varying2:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_text_array(data_slot, size_of_single_element,
            /* Unlike normal VARCHAR field values, VARCHAR array elements are
             * stored at a constant length, but padded with null characters: */
            '\0'
          )
      );
    break;

  case blr_short:
    CONV_IN_ARRAY_ELEMENT_CONVERT_INTEGER_TYPE(SQL_SHORT);
    break;

  case blr_long:
    CONV_IN_ARRAY_ELEMENT_CONVERT_INTEGER_TYPE(SQL_LONG);
    break;

  #ifdef INTERBASE_6_OR_LATER
  case blr_int64:
    CONV_IN_ARRAY_ELEMENT_CONVERT_INTEGER_TYPE(SQL_INT64);
    break;
  #endif /* INTERBASE_6_OR_LATER */

  case blr_float:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_float_array(py_input_converted, data_slot, cur)
      );
    break;

  case blr_double:
  case blr_d_float:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_double_array(py_input_converted, data_slot, cur)
      );
    break;

  case blr_timestamp:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_timestamp_array(py_input_converted, data_slot, cur)
      );
    break;

  #ifdef INTERBASE_6_OR_LATER
  case blr_sql_date:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_date_array(py_input_converted, data_slot, cur)
      );
    break;

  case blr_sql_time:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_time_array(py_input_converted, data_slot, cur)
      );
    break;
  #endif /* INTERBASE_6_OR_LATER */

  case blr_boolean_dtype:
    CONV_IN_ARRAY_ELEMENT_TRY_INPUT_CONVERSION(
        conv_in_boolean_array(py_input_converted, data_slot)
      );
    break;

  /* Currently, none of the following types is supported, because it's not
   * clear how one would create such a field via SQL DDL.  As far as I can
   * tell, this makes the types below useless for a client interface such as
   * kinterbasdb. */
  case blr_quad:
  case blr_blob:
  case blr_blob_id:
    raise_exception(NotSupportedError,
        "kinterbasdb does not support arrays of arrays or arrays of blobs"
        " because it's not clear how one would create such a field via SQL."
      );
    goto fail;

  /* NULL-terminated string: */
  case blr_cstring:
  case blr_cstring2:
    raise_exception(NotSupportedError,
        "kinterbasdb does not support blr_cstring arrays because it's not"
        " clear how one would create such a field via SQL, or even why it"
        " would be desirable (in light of the existence of CHAR and VARCHAR"
        " arrays)."
      );
    goto fail;

  default:
    raise_exception(NotSupportedError,
        "kinterbasdb does not support the input conversion of arrays of this"
        " type. " KIDB_REPORT " " KIDB_HOME_PAGE
      );
    goto fail;
  } /* end of switch on data_type */

  /* Success: */
  status = INPUT_OK;
  goto cleanup;

  fail:
    status = INPUT_ERROR;
    /* Fall through to cleanup. */

  cleanup:
    assert (status == INPUT_OK ?
        PyErr_Occurred() == NULL : PyErr_Occurred() != NULL
      );

    Py_XDECREF(py_input_converted);

    return status;
} /* conv_in_array_element */


#define _EXTRACT_SEQ__GET_SEQ_EL_WITH_EXCEPTION(seq, index, target) \
  target = PySequence_GetItem(seq, index); \
  if (target == NULL) { \
    PyObject *err_msg = PyString_FromFormat("Array input conversion:" \
        " unable to retrieve element %d of input sequence.", index \
      ); \
    if (err_msg == NULL) { goto fail; } \
    raise_exception(InterfaceError, PyString_AS_STRING(err_msg));  \
    Py_DECREF(err_msg); \
    goto fail; \
  }

static InputStatus _extract_pyseq_to_db_array_buffer(
    /* For validation: */
    PyObject *py_seq, short *dimension_sizes_ptr,

    /* For conversion: */
    char **data_slot,

    unsigned short dialect,
    short data_type, short data_subtype,
    size_t size_of_single_element, short scale,

    PyObject *converter,

    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle_p,
    Cursor *cur
  )
{
  InputStatus status;
  int i;
  int py_seq_len;
  int required_length_of_this_dimension = (int) *dimension_sizes_ptr;

  assert (*data_slot != NULL);
  assert (required_length_of_this_dimension > 0);

  if (!PySequence_Check(py_seq) || PyString_Check(py_seq)) {
    PyObject *input_py_obj_type = PyObject_Type(py_seq);
    if (input_py_obj_type != NULL) {
      PyObject *input_py_obj_type_repr = PyObject_Repr(input_py_obj_type);
      if (input_py_obj_type_repr != NULL) {
        PyObject *err_msg = PyString_FromFormat(
            "Array input conversion: type error:"
            " input sequence must be a Python sequence other than string, not"
            " a %s",
            PyString_AS_STRING(input_py_obj_type_repr)
          );
        if (err_msg != NULL) {
          raise_exception(InterfaceError, PyString_AS_STRING(err_msg));
          Py_DECREF(err_msg);
        }
        Py_DECREF(input_py_obj_type_repr);
      }
      Py_DECREF(input_py_obj_type);
    }

    goto fail;
  } /* if not appropriate seq */

  {
    const Py_ssize_t py_seq_len_ss = PySequence_Length(py_seq);
    if (py_seq_len_ss == -1) {
      goto fail;
    } else if (py_seq_len_ss > INT_MAX) {
      raise_exception(NotSupportedError, "The database API does not yet"
          " officially support arrays larger than 2 GB."
        );
      goto fail;
    }
    py_seq_len = (int) py_seq_len_ss;
  }

  if (py_seq_len != required_length_of_this_dimension) {
    PyObject *err_msg = PyString_FromFormat("Array input conversion: the input"
        " sequence is not appropriately shaped (current dimension requires"
        " input sequence of exactly %d elements, but actual input sequence"
        " has%s%d elements).%s",
        required_length_of_this_dimension,
        (py_seq_len < required_length_of_this_dimension ? " only " : ""),
        py_seq_len,
        (py_seq_len < required_length_of_this_dimension ? "  kinterbasdb"
            " cannot automatically pad too-short input sequences because the"
            " database engine does not allow elements of an array to be NULL."
            : ""
          )
      );
    if (err_msg != NULL) {
      raise_exception(InterfaceError, PyString_AS_STRING(err_msg));
      Py_DECREF(err_msg);
    }

    goto fail;
  } else {
    short *next_dimension_size_ptr = dimension_sizes_ptr + 1;
    if (*next_dimension_size_ptr == DIMENSION_SIZE_END_MARKER) {
      /* py_seq contains "leaf objects" (input values rather than
       * subsequences).  Convert each "leaf object" from its Pythonic form to
       * its DB-internal representation; store the result in the raw array
       * source buffer. */
      int conv_status_for_this_value;

      for (i = 0; i < py_seq_len; i++) {
        PyObject *py_input;
        _EXTRACT_SEQ__GET_SEQ_EL_WITH_EXCEPTION(py_seq, i, py_input);

        /* MEAT: */
        assert (*data_slot != NULL);
        conv_status_for_this_value = conv_in_array_element(
            py_input, data_slot,

            dialect,
            data_type, data_subtype,
            size_of_single_element, scale,
            converter,

            status_vector, db_handle, trans_handle_p,
            cur
          );

        /* PySequence_GetItem creates new ref; discard it: */
        Py_DECREF(py_input);

        if (conv_status_for_this_value != INPUT_OK) { goto fail; }

        /* Move the raw-array-source-buffer pointer to the next slot. */
        *data_slot += size_of_single_element;
      } /* end of convert-each-value loop */
    } else {
      /* py_seq does NOT contain "leaf objects", so recursively validate each
       * subsequence of py_seq. */
      for (i = 0; i < py_seq_len; i++) {
        int status_for_this_sub_seq;
        PyObject *sub_seq;

        _EXTRACT_SEQ__GET_SEQ_EL_WITH_EXCEPTION(py_seq, i, sub_seq);

        /* MEAT: */
        status_for_this_sub_seq = _extract_pyseq_to_db_array_buffer(
            sub_seq,
            next_dimension_size_ptr,

            data_slot,

            dialect,
            data_type, data_subtype,
            size_of_single_element, scale,
            converter,

            status_vector, db_handle, trans_handle_p,
            cur
          );

        /* PySequence_GetItem creates new ref; discard it: */
        Py_DECREF(sub_seq);

        if (status_for_this_sub_seq != INPUT_OK) { goto fail; }
      } /* end of validate-each-subsequence loop */
    } /* end of this-sequence-contains-leaves if block */
  } /* end of this-sequence-was-appropriate length if block */

  status = INPUT_OK;
  goto cleanup;

  fail:
    status = INPUT_ERROR;
    /* Fall through to cleanup. */

  cleanup:
    assert (status == INPUT_OK ?
        PyErr_Occurred() == NULL : PyErr_Occurred() != NULL
      );

    return status;
} /* _extract_pyseq_to_db_array_buffer */


/******************** INPUT FUNCTIONS:END ********************/

/******************** OUTPUT FUNCTIONS:BEGIN ********************/

static PyObject *conv_out_array(
    Cursor *cursor, short sqlvar_index,
    ISC_QUAD *array_id,
    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle_p,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  )
{
  /* Arrays are always stored in row-major order inside the DB, and are also
   * retrieved that way unless the app requests otherwise (via
   * desc->array_desc_flags). */
  PyObject *result = NULL;

  char *output_buf = NULL, *output_buf_walker;
  size_t output_buf_size = 0;
  size_t size_of_single_element = 0;
  short scale = -1;
  short data_type = -1;

  unsigned short number_of_dimensions;
  short *dimensions = NULL;

  int total_number_of_elements; /* Will be set later. */

  ISC_ARRAY_DESC *desc = _look_up_array_descriptor(cursor->trans,
      rel_name, rel_name_length,
      field_name, field_name_length
    );
  if (desc == NULL) { goto fail; }

  number_of_dimensions = desc->array_desc_dimensions;
  assert (number_of_dimensions >= 1);

  data_type = desc->array_desc_dtype;

  size_of_single_element = desc->array_desc_length;

  _ADJUST_ELEMENT_SIZE_FOR_VARCHAR_IF_NEC(data_type, size_of_single_element);

  scale = desc->array_desc_scale;

  /* Populate the short-array named dimensions.  (The _extract_dimensions_sizes
   * function also sets total_number_of_elements to its appropriate value.) */
  dimensions = _extract_dimensions_sizes(desc, &total_number_of_elements);
  if (dimensions == NULL) { goto fail; }
  /* The database engine doesn't allow zero-element arrays. */
  assert (total_number_of_elements > 0);

  output_buf_size = size_of_single_element * total_number_of_elements;

  output_buf = kimem_main_malloc(output_buf_size);
  if (output_buf == NULL) { goto fail; }
  output_buf_walker = output_buf;

  ENTER_GDAL
  isc_array_get_slice(status_vector,
      db_handle, trans_handle_p,
      array_id, desc,
      (void *) output_buf, (ISC_LONG *) &output_buf_size
    );
  LEAVE_GDAL
  if (DB_API_ERROR(status_vector)) { goto fail_operationerror; }

  /* The MEAT: */
  result = _extract_db_array_buffer_to_pyseq(
      cursor, sqlvar_index,
      /* Pointer to pointer to the first element of the output buffer: */
      &output_buf_walker,
      /* Pointer to array containing the element counts for successive
       * dimensions: */
      dimensions,

      /* Boilerplate parameters: */
      data_type, size_of_single_element, scale,
      /* Pass through utility stuff from above: */
      status_vector, db_handle, trans_handle_p,
      rel_name, rel_name_length, field_name, field_name_length
    );
  if (result == NULL) { goto fail; }

  /* Output buffer should've been filled entirely. */
  assert (((size_t) (output_buf_walker - output_buf)) == output_buf_size);

  /* We've retrieved the array successfully; now clean up. */
  goto cleanup;

  fail_operationerror:
    raise_sql_exception(OperationalError, "Array output conversion: ",
        status_vector
      );
    /* Fall through to fail. */

  fail:
    if (result != NULL) {
      Py_DECREF(result);
      result = NULL;
    }
    /* Fall though to cleanup. */

  cleanup:
    #ifdef USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS
    if (desc != NULL) { kimem_main_free(desc); }
    #endif

    if (dimensions != NULL) { kimem_main_free(dimensions); }
    if (output_buf != NULL) { kimem_main_free(output_buf); }

    assert (PyErr_Occurred() ? result == NULL : result != NULL);

    return result;
} /* conv_out_array */

static PyObject *_extract_db_array_buffer_to_pyseq(
    Cursor *cursor, short sqlvar_index,
    char **data_slot, short *dimension_sizes_ptr,

    /* Boilerplate parameters (capitalized to differentiate them): */
    short data_type, size_t size_of_single_element,
    short scale,
    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle_p,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  )
{
  short elCount = *dimension_sizes_ptr;
  short *next_dimension_size_ptr = dimension_sizes_ptr + 1;
  short i;
  PyObject *seq = PyList_New(elCount);
  if (seq == NULL) { goto fail; }

  if (*next_dimension_size_ptr == DIMENSION_SIZE_END_MARKER) {
    /* We're dealing with a "leaf" which contains actual output values rather
     * than subsequences. */
    for (i = 0; i < elCount; i++) {
      PyObject *val = conv_out_array_element(
          cursor, sqlvar_index,
          *data_slot,

          data_type, size_of_single_element,
          scale,
          status_vector, db_handle, trans_handle_p,
          rel_name, rel_name_length, field_name, field_name_length
        );
      if (val == NULL) { goto fail; }

      /* Move the raw-array-desination-buffer pointer to the next slot. */
      *data_slot += size_of_single_element;

      /* PyList_SET_ITEM steals ref to val; no need to DECREF. */
      PyList_SET_ITEM(seq, i, val);
    }
  } else {
    /* We're dealing with a subsequence rather than a leaf, so recurse. */
    for (i = 0; i < elCount; i++ ) {
      PyObject *subList = _extract_db_array_buffer_to_pyseq(
          cursor, sqlvar_index,
          data_slot,
          next_dimension_size_ptr,

          /* Boilerplate parameters (capitalized to differentiate them): */
          data_type, size_of_single_element, scale,
          status_vector, db_handle, trans_handle_p,
          rel_name, rel_name_length, field_name, field_name_length
        );
      if (subList == NULL) { goto fail; }

      /* PyList_SET_ITEM steals ref to subList; no need to DECREF. */
      PyList_SET_ITEM(seq, i, subList);
    }
  }

  assert (PyList_GET_SIZE(seq) == elCount);

  return seq;

  fail:
    assert (PyErr_Occurred());

    Py_XDECREF(seq);

    return NULL;
} /* _extract_db_array_buffer_to_pyseq */

static PyObject *conv_out_array_element(
    Cursor *cursor, short sqlvar_index,
    char *data,

    short data_type, size_t size_of_single_element,

    short scale,

    ISC_STATUS *status_vector,
    isc_db_handle *db_handle, isc_tr_handle *trans_handle_p,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  )
{
  PyObject *result = NULL;
  PyObject *converter = NULL;
  const unsigned short dialect = Transaction_get_dialect(cursor->trans);

  const short data_subtype = _determine_sqlsubtype_for_array(cursor->trans,
      rel_name, rel_name_length, field_name, field_name_length
    );
  if (data_subtype == SQLSUBTYPE_DETERMINATION_ERROR) { goto fail; }
  Transaction_stats_record_ps_executed(cursor->trans);

  converter = cursor_get_out_converter(cursor, sqlvar_index,
      data_type, data_subtype, scale, TRUE
    );
  /* cursor_get_out_converter returns NULL on error, borrowed reference to
   * Py_None if there was no converter. */
  if (converter == NULL) { goto fail; }

  switch (data_type) {

  case blr_text:
  case blr_text2:
    result = conv_out_char(data, size_of_single_element);
    break;

  case blr_varying:
  case blr_varying2:
    { /* VARCHAR array elements are stored differently from the way
       * conventional VARCHAR fields are stored (see documentary note about
       * _ADJUST_ELEMENT_SIZE_FOR_VARCHAR_IF_NEC). */
      const size_t len_before_null = strlen(data);
      result = conv_out_char(data,
          (len_before_null <= size_of_single_element ?
              len_before_null
            : size_of_single_element
          )
        );
    }
    break;

  /* NULL-terminated string: */
  case blr_cstring:
  case blr_cstring2:
    result = PyString_FromString(data);
    break;

  case blr_short:
    result = conv_out_short_long(data,
        SQL_SHORT,
        IS_FIXED_POINT__ARRAY_EL(dialect, data_type, data_subtype, scale),
        scale
      );
    break;

  case blr_long:
    result = conv_out_short_long(data,
        SQL_LONG,
        IS_FIXED_POINT__ARRAY_EL(dialect, data_type, data_subtype, scale),
        scale
      );
    break;

  #ifdef INTERBASE_6_OR_LATER
  case blr_int64:
    result = conv_out_int64(data,
        IS_FIXED_POINT__ARRAY_EL(dialect, data_type, data_subtype, scale),
        scale
      );
    break;
  #endif /* INTERBASE_6_OR_LATER */

  case blr_float:
    result = conv_out_floating(*((float *) data), dialect, scale);
    break;

  case blr_double:
  case blr_d_float:
    result = conv_out_floating(*((double *) data), dialect, scale);
    break;

  case blr_timestamp:
    result = conv_out_timestamp(data);
    break;

  #ifdef INTERBASE_6_OR_LATER
  case blr_sql_date:
    result = conv_out_date(data);
    break;

  case blr_sql_time:
    result = conv_out_time(data);
    break;
  #endif /* INTERBASE_6_OR_LATER */

  case blr_boolean_dtype:
    result = conv_out_boolean(data);
    break;

  case blr_quad:
    /* ISC_QUAD structure; since the DB engine doesn't support arrays of
     * arrays, assume that this item refers to a blob id. */
  case blr_blob:
  case blr_blob_id:
    raise_exception(NotSupportedError,
        "kinterbasdb does not support arrays of arrays or arrays of blobs"
        " because it's not clear how one would create such a field via SQL."
      );
    goto fail;

  default:
    raise_exception(NotSupportedError,
        "kinterbasdb does not support the output conversion of arrays of this"
        " type. " KIDB_REPORT " " KIDB_HOME_PAGE
      );
    goto fail;
  }

  assert (converter != NULL); /* Can't be NULL; may be None. */
  /* Obviously mustn't invoke the converter if the original value was not
   * loaded properly from the database. */
  if (result != NULL) {
    /* Replacing the PyObject pointer in result is not a refcount leak; see the
     * comments in dynamically_type_convert_output_obj_if_necessary. */
    result = dynamically_type_convert_output_obj_if_necessary(
        result, converter, data_type, data_subtype
      );
  } else {
    assert (PyErr_Occurred());
  }

  return result;

  fail:
    assert (PyErr_Occurred());

    Py_XDECREF(result);
    /* The reference to 'converter' is borrowed; no need to DECREF. */

    return NULL;
} /* conv_out_array_element */

/******************** OUTPUT FUNCTIONS:END ********************/

/******************** UTILITY FUNCTIONS:BEGIN ********************/

#ifdef USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS
static ISC_ARRAY_DESC *_look_up_array_descriptor(
    Transaction *trans,
    /* These strings aren't null-terminated: */
    char *sqlvar_rel_name, short sqlvar_rel_name_length,
    char *sqlvar_field_name, short sqlvar_field_name_length
  )
{
  ISC_STATUS *status_vector = Transaction_get_sv(trans);
  isc_db_handle *db_handle = Transaction_get_db_handle_p(trans);
  isc_tr_handle *trans_handle_p = Transaction_get_handle_p(trans);

  /* isc_array_lookup_* functions require null-terminated strings, but the
   * relevant strings from the XSQLVAR structure are not null-terminated. */
  char *null_terminated_table_name = NULL, *null_terminated_field_name = NULL;

  /* Begin initial memory allocation section. */
  ISC_ARRAY_DESC *desc = kimem_main_malloc(sizeof(ISC_ARRAY_DESC));
  if (desc == NULL) { goto fail; }

  null_terminated_table_name = kimem_main_malloc(sqlvar_rel_name_length + 1);
  if (null_terminated_table_name == NULL) { goto fail; }

  null_terminated_field_name = kimem_main_malloc(sqlvar_field_name_length + 1);
  if (null_terminated_field_name == NULL) { goto fail; }
  /* End initial memory allocation section. */

  /* Copy the non-null-terminated strings sqlvar_rel_name and
   * sqlvar_field_name into the null-terminated strings null_terminated_*_name. */
  memcpy(null_terminated_table_name, sqlvar_rel_name, sqlvar_rel_name_length);
  null_terminated_table_name[sqlvar_rel_name_length] = '\0';
  memcpy(null_terminated_field_name, sqlvar_field_name, sqlvar_field_name_length);
  null_terminated_field_name[sqlvar_field_name_length] = '\0';

  ENTER_GDAL
  isc_array_lookup_bounds(status_vector,
      db_handle, trans_handle_p,
      null_terminated_table_name, null_terminated_field_name,
      desc
    );
  LEAVE_GDAL
  if (DB_API_ERROR(status_vector)) {
    raise_sql_exception(OperationalError, "Array descriptor lookup: ",
        status_vector
      );
    goto fail;
  }

  /* Successfully completed. */
  assert (desc != NULL);
  goto cleanup;

  fail:
    assert (PyErr_Occurred());

    if (desc != NULL) {
      kimem_main_free(desc);
      desc = NULL;
    }
    /* Fall through to cleanup. */

  cleanup:
    if (null_terminated_table_name != NULL) {
      kimem_main_free(null_terminated_table_name);
    }
    if (null_terminated_field_name != NULL) {
      kimem_main_free(null_terminated_field_name);
    }

    return desc;
} /* _look_up_array_descriptor */
#else
static ISC_ARRAY_DESC *_look_up_array_descriptor(
    Transaction *trans,
    /* These strings aren't null-terminated: */
    char *sqlvar_rel_name, short sqlvar_rel_name_length,
    char *sqlvar_field_name, short sqlvar_field_name_length
  )
{
  PyObject *py_desc = NULL;
  ISC_ARRAY_DESC *desc = NULL;

  PyObject *py_rel_name = NULL;
  PyObject *py_field_name = NULL;

  assert (trans != NULL);
  assert (trans->con_python_wrapper != NULL);

  py_rel_name = PyString_FromStringAndSize(
      sqlvar_rel_name, sqlvar_rel_name_length
    );
  if (py_rel_name == NULL) { goto fail; }

  py_field_name = PyString_FromStringAndSize(
      sqlvar_field_name, sqlvar_field_name_length
    );
  if (py_field_name == NULL) { goto fail; }

  py_desc = PyObject_CallFunctionObjArgs(py_look_up_array_descriptor,
      trans->con_python_wrapper, py_rel_name, py_field_name, NULL
    );
  if (py_desc == NULL) { goto fail; }

  if (!PyString_CheckExact(py_desc)) {
    raise_exception(InternalError,
        "py_look_up_array_descriptor returned wrong type."
      );
    goto fail;
  }

  /* desc is just a pointer to the internal buffer of py_desc.  The
   * connection's cache will maintain a reference to py_desc at least as long
   * as we need to use desc, so there are no object lifetime problems. */
  assert (py_desc->ob_refcnt > 1);
  desc = (ISC_ARRAY_DESC *) PyString_AS_STRING(py_desc);

  goto clean;
  fail:
    assert (PyErr_Occurred());
    desc = NULL;
    /* Fall through to clean: */
  clean:
    Py_XDECREF(py_desc); /* Yes, this is correct--see lifetime note above. */
    Py_XDECREF(py_rel_name);
    Py_XDECREF(py_field_name);

    return desc;
} /* _look_up_array_descriptor */
#endif

static short *_extract_dimensions_sizes(
    ISC_ARRAY_DESC *desc,
    /* output param: */
    int *total_number_of_elements
  )
{
  int dimension;
  unsigned short number_of_dimensions = desc->array_desc_dimensions;
  ISC_ARRAY_BOUND bounds_of_current_dimension;

  /* Populate the short-array dimensions, and calculate the total number of
   * elements: */
  short *dimensions = kimem_main_malloc((number_of_dimensions + 1) * sizeof(short));
  if (dimensions == NULL) { goto fail; }

  *total_number_of_elements = 1;
  for (dimension = 0; dimension < number_of_dimensions; dimension++) {
    bounds_of_current_dimension = desc->array_desc_bounds[dimension];
    dimensions[dimension] =
          (bounds_of_current_dimension.array_bound_upper + 1)
         - bounds_of_current_dimension.array_bound_lower
      ;

    *total_number_of_elements *= dimensions[dimension];
  }

  /* The final element is set to a flag value (for pointer-walking
   * convenience). */
  dimensions[number_of_dimensions] = DIMENSION_SIZE_END_MARKER;

  return dimensions;

  fail:
    assert (PyErr_Occurred());

    if (dimensions != NULL) { kimem_main_free(dimensions); }

    return NULL;
} /* _extract_dimensions_sizes */

static short _determine_sqlsubtype_for_array(
    Transaction *trans,
    char *rel_name, short rel_name_length,
    char *field_name, short field_name_length
  )
{
  #ifdef USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS
    ISC_STATUS *status_vector = Transaction_get_sv(trans);
    isc_db_handle *db_handle = Transaction_get_db_handle_p(trans);
    isc_tr_handle *trans_handle_p = Transaction_get_handle_p(trans);
    short out_var_sqlind = SQLIND_NULL;

    /* Returns the subtype on success, or SQLSUBTYPE_DETERMINATION_ERROR on error. */
    const char *subtype_determination_statement =
         "SELECT FIELD_SPEC.RDB$FIELD_SUB_TYPE"
        " FROM RDB$FIELDS FIELD_SPEC, RDB$RELATION_FIELDS REL_FIELDS"
        " WHERE"
              " FIELD_SPEC.RDB$FIELD_NAME = REL_FIELDS.RDB$FIELD_SOURCE"
          " AND REL_FIELDS.RDB$RELATION_NAME = ?"
          " AND REL_FIELDS.RDB$FIELD_NAME = ?"
      ;
    XSQLDA *out_da = NULL;
    XSQLDA *in_da = NULL;
    XSQLVAR *in_var = NULL;
    XSQLVAR *out_var = NULL;
    isc_stmt_handle stmt_handle_sqlsubtype = NULL;

    /* Guilty until proven innocent: */
    short sqlsubtype = SQLSUBTYPE_DETERMINATION_ERROR;

    /* There's no need to use dynamic allocation here, but since this code is
     * disabled anyway due to the concurrency bug in isc_array_lookup_bounds,
     * I won't bother optimizing it: */
    in_da = kimem_xsqlda_malloc(XSQLDA_LENGTH(2));
    if (in_da == NULL) {
      /* Weren't calling one of the Python-supplied malloc impls; need to set
       * MemoryError. */
      PyErr_NoMemory();
      goto fail;
    }

    in_da->version = SQLDA_VERSION_KIDB;
    in_da->sqln = 2;
    in_da->sqld = 2;

    in_da->sqlvar      ->sqltype = SQL_TEXT;
    (in_da->sqlvar + 1)->sqltype = SQL_TEXT;

    /* Set the names of the relation.field for which we're determining
     * sqlsubtype. */
    in_var = in_da->sqlvar; /* First input variable. */
    in_var->sqllen = rel_name_length;
    in_var->sqldata = rel_name;

    in_var++; /* Second input variable. */
    in_var->sqllen = field_name_length;
    in_var->sqldata = field_name;

    /* Set up the output structures.  We know at design time exactly how they
     * should be configured; there's no convoluted dance of dynamism here, as
     * there is in servicing a generic Python-level query. */

    /* There's no need to use dynamic allocation here, but since this code is
     * disabled anyway due to the concurrency bug in isc_array_lookup_bounds,
     * I won't bother optimizing it: */
    out_da = (XSQLDA *) kimem_xsqlda_malloc(XSQLDA_LENGTH(1));
    if (out_da == NULL) {
      /* Weren't calling one of the Python-supplied malloc impls; need to set
       * MemoryError. */
      PyErr_NoMemory();
      goto fail;
    }

    out_da->version = SQLDA_VERSION_KIDB;
    out_da->sqln = 1;

    out_var = out_da->sqlvar;

    out_var->sqldata = (char *) kimem_main_malloc(sizeof(short));
    if (out_var->sqldata == NULL) { goto fail; }

    out_var->sqlind = &out_var_sqlind;

    ENTER_GDAL
    isc_dsql_allocate_statement(status_vector,
        db_handle, &stmt_handle_sqlsubtype
      );
    LEAVE_GDAL
    if (DB_API_ERROR(status_vector)) { goto fail; }

    ENTER_GDAL
    isc_dsql_prepare(status_vector,
        trans_handle_p, &stmt_handle_sqlsubtype,
        0, (char *) subtype_determination_statement, 3,
        out_da
      );
    LEAVE_GDAL
    if (DB_API_ERROR(status_vector)) { goto fail; }

    ENTER_GDAL
    isc_dsql_execute2(status_vector,
        trans_handle_p, &stmt_handle_sqlsubtype,
        3, in_da, out_da
      );
    LEAVE_GDAL
    if (DB_API_ERROR(status_vector)) { goto fail; }

    if (out_var_sqlind == SQLIND_NULL) {
      sqlsubtype = 0;
    } else {
      sqlsubtype = *((short *) out_var->sqldata);
    }

    goto cleanup;
    fail:
      if (DB_API_ERROR(status_vector)) {
        raise_sql_exception(InternalError, "_determine_sqlsubtype_for_array: ",
            status_vector
          );
      }
      assert (PyErr_Occurred());
      /* Fall through to cleanup. */

    cleanup:
      if (stmt_handle_sqlsubtype != NULL) {
        /* The isc_dsql_free_statement call here is relatively safe because under
         * normal circumstances the connection is unlikely to be severed between
         * the time the statement handle is allocated with
         * isc_dsql_allocate_statement (earlier in this same function) and the
         * time it's freed (here). */
        ENTER_GDAL
        isc_dsql_free_statement(status_vector, &stmt_handle_sqlsubtype, DSQL_drop);
        LEAVE_GDAL
      }

      if (in_da != NULL) { kimem_xsqlda_free(in_da); }

      if (out_da != NULL) {
        if (out_da->sqlvar->sqldata != NULL) {
          kimem_main_free(out_da->sqlvar->sqldata);
        }
        kimem_xsqlda_free(out_da);
      }

      return sqlsubtype;
  #else /* !USE_OFFICIAL_ISC_ARRAY_LOOKUP_BOUNDS: */
    PyObject *py_sqlsubtype = NULL;
    short sqlsubtype = SQLSUBTYPE_DETERMINATION_ERROR;

    PyObject *py_rel_name = NULL;
    PyObject *py_field_name = NULL;

    assert (trans != NULL);
    assert (trans->con_python_wrapper != NULL);

    py_rel_name = PyString_FromStringAndSize(rel_name, rel_name_length);
    if (py_rel_name == NULL) { goto fail; }

    py_field_name = PyString_FromStringAndSize(field_name, field_name_length);
    if (py_field_name == NULL) { goto fail; }

    py_sqlsubtype = PyObject_CallFunctionObjArgs(py_look_up_array_subtype,
        trans->con_python_wrapper, py_rel_name, py_field_name, NULL
      );
    if (py_sqlsubtype == NULL) { goto fail; }

    if (py_sqlsubtype == Py_None) {
      sqlsubtype = 0;
    } else {
      if (!PyInt_CheckExact(py_sqlsubtype)) {
        raise_exception(InternalError,
            "py_look_up_array_subtype returned wrong type."
          );
        goto fail;
      } else {
        const long sqlsubtype_long = PyInt_AS_LONG(py_sqlsubtype);
        assert (sqlsubtype_long >= 0);
        assert (sqlsubtype_long <= SHRT_MAX);
        sqlsubtype = (short) sqlsubtype_long;
      }
    }

    goto clean;
    fail:
      assert (PyErr_Occurred());
      assert (sqlsubtype == -1);
      /* Fall through to clean: */
    clean:
      Py_XDECREF(py_sqlsubtype);
      Py_XDECREF(py_rel_name);
      Py_XDECREF(py_field_name);

      return sqlsubtype;
  #endif
} /* _determine_sqlsubtype_for_array */

/******************** UTILITY FUNCTIONS:END ********************/
