/* KInterbasDB Python Package - Implementation of Dynamic Type Translation
 *
 * 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 PyObject *dynamically_type_convert_input_obj_if_necessary(
    PyObject *py_input,
    boolean is_array_element,
    unsigned short dialect,
    short data_type, short data_subtype, short scale,
    PyObject *converter
  );

/* Infinitely peristent global variables: */
PyObject *_type_names_all_supported;

PyObject *cached_type_name_TEXT;
PyObject *cached_type_name_TEXT_UNICODE;
PyObject *cached_type_name_BLOB;

PyObject *cached_type_name_INTEGER;
PyObject *cached_type_name_FIXED;
PyObject *cached_type_name_FLOATING;

PyObject *cached_type_name_TIMESTAMP;
PyObject *cached_type_name_DATE;
PyObject *cached_type_name_TIME;

PyObject *cached_type_name_BOOLEAN;

#define DTT_DISALLOW_POSITIONAL -1
#define DTT_POSITIONAL_IN_RANGE(i) (i >= 0 && i <= SHRT_MAX)

#define IS_UNICODE_CHAR_OR_VARCHAR(data_type, data_subtype) \
  ((boolean) ( \
    ((data_type) == SQL_VARYING || (data_type) == SQL_TEXT) \
    && (data_subtype) > 2 \
  ))

#define CACHED_TYPE_NAME__TEXT_OR_TEXT_UNICODE(data_subtype) \
  ( ((data_subtype) <= 2) ? \
        cached_type_name_TEXT \
      : cached_type_name_TEXT_UNICODE \
  )

static int init_kidb_type_translation(void) {
  _type_names_all_supported = PyList_New(0);
  if (_type_names_all_supported == NULL) { goto fail; }

  /* ICTN stands for Init Constant Type Name: */
  #define _ICTN(ptr, name) \
    if ((ptr = PyString_FromString(name)) == NULL) { goto fail; } \
    if (PyList_Append(_type_names_all_supported, ptr) != 0) { goto fail; }

  _ICTN( cached_type_name_TEXT,           "TEXT"                            );
  _ICTN( cached_type_name_TEXT_UNICODE,   "TEXT_UNICODE"                    );
  _ICTN( cached_type_name_BLOB,           "BLOB"                            );

  _ICTN( cached_type_name_INTEGER,        "INTEGER"                         );
  _ICTN( cached_type_name_FIXED,          "FIXED"                           );
  _ICTN( cached_type_name_FLOATING,       "FLOATING"                        );

  _ICTN( cached_type_name_TIMESTAMP,      "TIMESTAMP"                       );
  _ICTN( cached_type_name_DATE,           "DATE"                            );
  _ICTN( cached_type_name_TIME,           "TIME"                            );

  _ICTN( cached_type_name_BOOLEAN,        "BOOLEAN"                         );

  return 0;

  fail:
    assert (PyErr_Occurred());

    /* Don't free any allocated memory because this function is called during
     * module import, and Python provides no way to recover from that an error
     * during that process anyway. */

    return -1;
} /* init_kidb_type_translation */


static PyObject *_get_cached_type_name_for_conventional_code(
    unsigned short dialect, short data_type, short data_subtype, short scale
  )
{
  switch (data_type) {
    case SQL_TEXT:
    case SQL_VARYING:
      return CACHED_TYPE_NAME__TEXT_OR_TEXT_UNICODE(data_subtype);

    case SQL_BLOB:
      return cached_type_name_BLOB;

    case SQL_SHORT:
    case SQL_LONG:
    #ifdef INTERBASE_6_OR_LATER
    case SQL_INT64:
    #endif /* INTERBASE_6_OR_LATER */
      return
            IS_FIXED_POINT__CONVENTIONAL(dialect, data_type, data_subtype, scale)
          ? cached_type_name_FIXED
          : cached_type_name_INTEGER
        ;

    case SQL_FLOAT:
    case SQL_DOUBLE:
    case SQL_D_FLOAT:
      /* 2005.07.11:  Internal floating point value can "logically" represent
       * fixed-point value in dialect < 3 databases. */
      if (dialect < 3 && scale != 0) {
        return cached_type_name_FIXED;
      } else {
        return cached_type_name_FLOATING;
      }

    case SQL_TIMESTAMP:
      return cached_type_name_TIMESTAMP;
    #ifdef INTERBASE_6_OR_LATER
    case SQL_TYPE_DATE:
      return cached_type_name_DATE;
    case SQL_TYPE_TIME:
      return cached_type_name_TIME;
    #endif /* INTERBASE_6_OR_LATER */

    case SQL_BOOLEAN:
      return cached_type_name_BOOLEAN;

    default:
      {
        PyObject *err_msg = PyString_FromFormat(
            "Unable to determine conventional type name from these parameters:"
            "  dialect: %d, data_type: %d, data_subtype: %d, scale: %d",
            (int) dialect, (int) data_type, (int) data_subtype,  (int) scale
          );
        if (err_msg != NULL) {
          raise_exception(InternalError, PyString_AS_STRING(err_msg));
          Py_DECREF(err_msg);
        }
      }
      return NULL;
  }
} /* _get_cached_type_name_for_conventional_code */


static PyObject *_get_cached_type_name_for_array_code(
    unsigned short dialect, short data_type, short data_subtype, short scale
  )
{
  switch (data_type) {
    case blr_text:
    case blr_text2:
    case blr_varying:
    case blr_varying2:
    case blr_cstring:
    case blr_cstring2:
      return CACHED_TYPE_NAME__TEXT_OR_TEXT_UNICODE(data_subtype);

    case blr_short:
    case blr_long:
    #ifdef INTERBASE_6_OR_LATER
    case blr_int64:
    #endif /* INTERBASE_6_OR_LATER */
      return
            IS_FIXED_POINT__ARRAY_EL(dialect, data_type, data_subtype, scale)
          ? cached_type_name_FIXED
          : cached_type_name_INTEGER
        ;

    case blr_float:
    case blr_double:
    case blr_d_float:
      /* 2005.07.11:  Internal floating point value can "logically" represent
       * fixed-point value in dialect < 3 databases. */
      if (dialect < 3 && scale != 0) {
        return cached_type_name_FIXED;
      } else {
        return cached_type_name_FLOATING;
      }

    case blr_timestamp:
      return cached_type_name_TIMESTAMP;
    #ifdef INTERBASE_6_OR_LATER
    case blr_sql_date:
      return cached_type_name_DATE;
    case blr_sql_time:
      return cached_type_name_TIME;
    #endif /* INTERBASE_6_OR_LATER */

    case blr_boolean_dtype:
      return cached_type_name_BOOLEAN;

    case blr_quad:
      /* The database engine does not support arrays of arrays. */
    case blr_blob:
    case blr_blob_id:
      /* The database engine does not support arrays of blobs. */
      raise_exception(InternalError, "_get_cached_type_name_for_array_code:"
          " This code was written under the assumption that the database"
          " engine does not support arrays of arrays or arrays of blobs."
        );
      return NULL;

    default:
      {
        PyObject *err_msg = PyString_FromFormat(
            "Unable to determine array type name from these parameters:"
            "  dialect: %d, data_type: %d, data_subtype: %d, scale: %d",
            (int) dialect, (int) data_type, (int) data_subtype,  (int) scale
          );
        if (err_msg != NULL) {
          raise_exception(InternalError, PyString_AS_STRING(err_msg));
          Py_DECREF(err_msg);
        }
      }
      return NULL;
  }
} /* _get_cached_type_name_for_array_code */


static PyObject *_get_converter(
    PyObject *trans_dict,
    short sqlvar_index,
    unsigned short dialect,
    short data_type, short data_subtype, short scale,
    boolean is_array_field
  )
{
  /* Returns a borrowed reference to the converter if one is registered in
   * $trans_dict for the specified type  (the registered converter might be
   * Py_None to mandate "naked" translation).
   * If no converter was present, returns NULL.
   * Also returns NULL on error, so use PyErr_Occurred() to determine whether
   * the NULL return value indicates "missing" or "error." */
  PyObject *converter = NULL;

  if (trans_dict != NULL) {
    PyObject *type_name = NULL;

    /* Positional DTT settings take precedence over typal settings, but of
     * course positional settings only apply to cursors, not connections. */
    if (sqlvar_index != DTT_DISALLOW_POSITIONAL) {
      PyObject *py_sqlvar_index = PyInt_FromLong(sqlvar_index);
      if (py_sqlvar_index == NULL) { goto fail; }
      converter = PyDict_GetItem(trans_dict, py_sqlvar_index);
      Py_DECREF(py_sqlvar_index);
      if (converter != NULL) {
        return converter;
      } /* Else, fall through to typal lookup. */
    }

    type_name = (
        is_array_field ?
            _get_cached_type_name_for_array_code(dialect,
                data_type, data_subtype, scale
              )
          : _get_cached_type_name_for_conventional_code(dialect,
                data_type, data_subtype, scale
              )
      );
    if (type_name == NULL) { goto fail; }
    converter = PyDict_GetItem(trans_dict, type_name);
    if (converter != NULL) {
      return converter;
    }
  }

  /* Converter couldn't be found (not an error). */
  assert (converter == NULL);
  return NULL;

  fail:
    /* An exception should've already been set: */
    assert (PyErr_Occurred());
    return NULL;
} /* _get_converter */

#define _make__connection_get_DIRECTION_converter(direction) \
  static PyObject *connection_get_ ## direction ## _converter( \
      CConnection *con, \
      short data_type, short data_subtype, short scale, \
      boolean is_array_field \
    ) \
  { \
    PyObject *converter = _get_converter(con->type_trans_ ## direction, \
        /* Positional DTT doesn't apply to connections: */ \
        DTT_DISALLOW_POSITIONAL, \
        con->dialect, \
        data_type, data_subtype, scale, is_array_field \
      ); \
    if (converter == NULL && !PyErr_Occurred()) { \
      /* No converter was present; return borrowed ref to None. */ \
      converter = Py_None; \
    } \
    return converter; \
  }

_make__connection_get_DIRECTION_converter(out)
_make__connection_get_DIRECTION_converter(in)


#define _make__cursor_get_DIRECTION_converter(direction) \
  static PyObject *cursor_get_ ## direction ## _converter( \
      Cursor *cursor, short sqlvar_index, \
      short data_type, short data_subtype, short scale, \
      boolean is_array_field \
    ) \
  { \
    PyObject *trans_dict = cursor->type_trans_ ## direction; \
    PyObject *converter = _get_converter(trans_dict, sqlvar_index, \
        Transaction_get_dialect(cursor->trans), \
        data_type, data_subtype, scale, is_array_field \
      ); \
    \
    if (converter != NULL || PyErr_Occurred()) { \
      return converter; \
    } \
    \
    /* Fall back on the connection's translation dictionary, if any. */ \
    return connection_get_ ## direction ## _converter( \
        Transaction_get_con(cursor->trans), \
        data_type, data_subtype, scale, is_array_field \
      ); \
  }

_make__cursor_get_DIRECTION_converter(in)
_make__cursor_get_DIRECTION_converter(out)


#define _make__cursor_get_INOROUT_converter_for_type_name(direction) \
  static PyObject *cursor_get_ ## direction ## _converter_for_type_name( \
      Cursor *cursor, PyObject *type_name \
    ) \
  { \
    /* Returns borrowed ref. */ \
    PyObject *converter; \
    PyObject *trans_dict = cursor->type_trans_ ## direction; \
    if (trans_dict != NULL) { \
      converter = PyDict_GetItem(trans_dict, type_name); \
      if (converter != NULL || PyErr_Occurred()) { \
        return converter; \
      } \
    } \
    /* Fall back on the connection's translation dictionary, if any. */ \
    trans_dict = Transaction_get_con(cursor->trans)->type_trans_ ## direction; \
    if (trans_dict != NULL) { \
      converter = PyDict_GetItem(trans_dict, type_name); \
      if (converter != NULL || PyErr_Occurred()) { \
        return converter; \
      } \
    } \
    return Py_None; \
  }

_make__cursor_get_INOROUT_converter_for_type_name(in)
_make__cursor_get_INOROUT_converter_for_type_name(out)


static PyObject *connection_get_translator_output_type(
    CConnection *con, PyObject *translator_key
  )
{
  /* Helper function for cursor_get_translator_output_type. */
  assert (PyString_Check(translator_key));
  {
    PyObject *output_type_dict = con->output_type_trans_return_type_dict;
    if (output_type_dict != NULL) {
      return PyDict_GetItem(output_type_dict, translator_key);
    }
    return NULL;
  }
} /* connection_get_translator_output_type */


/* cursor_get_translator_output_type's search might "bubble" to its connection
 * in a manner similar to the "bubble" in cursor_get_(in|out)_converter. */
static PyObject *cursor_get_translator_output_type(
    Cursor *cursor, short sqlvar_index, PyObject *translator_key
  )
{
  /* If a record of the return type of the output translator is found, return a
   * borrowed reference to that type.  Otherwise, return NULL (which simply
   * means "not found"--it doesn't mean there was an error unless
   * PyErr_Occurred()). */
  assert (PyString_Check(translator_key));
  {
    PyObject *output_type_dict = cursor->output_type_trans_return_type_dict;
    if (output_type_dict != NULL) {
      PyObject *output_type = NULL;

      /* Positional DTT settings take precedence over typal. */
      PyObject *py_sqlvar_index = PyInt_FromLong(sqlvar_index);
      if (py_sqlvar_index == NULL) { return NULL; }
      output_type = PyDict_GetItem(output_type_dict, py_sqlvar_index);
      Py_DECREF(py_sqlvar_index);

      if (output_type == NULL) {
        output_type = PyDict_GetItem(output_type_dict, translator_key);
      }

      if (output_type != NULL) {
        return output_type;
      } /* Else, fall through and search the connection's output type dict. */
    }

    return connection_get_translator_output_type(
        Transaction_get_con(cursor->trans), translator_key
      );
  }
} /* cursor_get_translator_output_type */


typedef enum {
  DTT_KEYS_ALL_VALID = 1,
  DTT_KEYS_INVALID = 0,
  DTT_KEYS_VALIDATION_PROBLEM = -1
} DTTKeyValidationResult;

static DTTKeyValidationResult _validate_dtt_keys(PyObject *trans_dict,
    boolean allow_positional
  )
{
  /* Returns:
   * - DTT_KEYS_ALL_VALID if all keys are valid (that is, all are recognized
   *   type name strings or integers between 0 and SHRT_MAX, inclusive).
   * - DTT_KEYS_INVALID if at least one key is invalid.
   * - DTT_KEYS_VALIDATION_PROBLEM upon error in validation process. */
  DTTKeyValidationResult status = DTT_KEYS_VALIDATION_PROBLEM;
  Py_ssize_t key_count;
  Py_ssize_t i;
  PyObject *keys = PyDict_Keys(trans_dict);
  if (keys == NULL) { goto fail; }

  key_count = PyList_GET_SIZE(keys);
  for (i = 0; i < key_count; i++) {
    /* PyList_GET_ITEM "returns" a borrowed ref, and can't fail as long as the
     * first argument is of the correct type, which it certainly is here. */
    PyObject *k = PyList_GET_ITEM(keys, i);

    if (allow_positional && PyInt_Check(k)) {
      const long c_k = PyInt_AS_LONG(k);
      if (!DTT_POSITIONAL_IN_RANGE(c_k)) {
        PyObject *err_msg = PyString_FromFormat("Positional DTT keys must be"
            " between 0 and %d (inclusive); %ld is outside that range.",
            SHRT_MAX, c_k
          );
        if (err_msg == NULL) { goto fail; }
        raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
        Py_DECREF(err_msg);
        status = DTT_KEYS_INVALID;
        goto fail;
      }
    } else if (PyUnicode_Check(k)) {
      /* Unicode DTT keys are forbidden here in a special case because if
       * they're included in the general validation case below, confusing error
       * messages that have to do with encoding rather than DTT tend to arise. */
      raise_exception(ProgrammingError, "unicode objects are not allowed as"
          " dynamic type translation keys."
        );
      status = DTT_KEYS_INVALID;
      goto fail;
    } else {
      const int contains = PySequence_Contains(_type_names_all_supported, k);
      if (contains == -1) {
        goto fail;
      } else if (contains == 0) {
        /* k was not in the master list of supported type names. */
        PyObject *err_msg = NULL;
        char *msg_template = NULL;

        PyObject *str_all_supported = NULL;
        PyObject *str_k = PyObject_Str(k);
        if (str_k == NULL) { goto fail; }
        str_all_supported = PyObject_Str(_type_names_all_supported);
        if (str_all_supported == NULL) {
          Py_DECREF(str_k);
          goto fail;
        }

        if (allow_positional) {
          msg_template = "Translator key '%s' is not valid. The key must be"
            " either a zero-based integer index (for positional DTT) or one"
            " of %s (for type-based DTT).";
        } else {
          msg_template = "Translator key '%s' is not valid. The key must be"
            " one of %s.";
        }
        err_msg = PyString_FromFormat(msg_template,
            PyString_AS_STRING(str_k), PyString_AS_STRING(str_all_supported)
          );
        Py_DECREF(str_k);
        Py_DECREF(str_all_supported);
        if (err_msg == NULL) { goto fail; }
        raise_exception(ProgrammingError, PyString_AS_STRING(err_msg));
        Py_DECREF(err_msg);

        status = DTT_KEYS_INVALID;
        goto fail;
      } /* else, contains == 1, which is the 'success' condition. */
    }
  }
  status = DTT_KEYS_ALL_VALID;
  goto cleanup;

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

  cleanup:
    Py_XDECREF(keys);

    return status;
} /* _validate_dtt_keys */


#define DICT_IS_NONE_OR_EMPTY(d) ((d) == Py_None || PyDict_Size(d) == 0)

/* Generic programming the ugly way: */
#define _make__type_trans_setter(direction_is_IN, \
    func_name, member_name, \
    type_name, type_infra_name, is_for_cursor \
  ) \
  static PyObject *func_name(PyObject *self, PyObject *args) { \
    type_name *target; \
    PyObject *trans_dict; \
    \
    if (is_for_cursor) { \
      target = (type_name *) self; \
      if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &trans_dict)) \
      { return NULL; } \
    } else { \
      if (!PyArg_ParseTuple(args, "O!O!", \
            &type_infra_name, &target, &PyDict_Type, &trans_dict \
         )) \
      { return NULL; } \
    } \
    if (_validate_dtt_keys(trans_dict, is_for_cursor) != DTT_KEYS_ALL_VALID) \
    { return NULL; } \
    \
    { \
      /* If a 'BLOB' entry is specified and the supplied translator is a \
       * dict, validate its contents. */ \
      PyObject *blob_trans = PyDict_GetItem(trans_dict, cached_type_name_BLOB); \
      if (blob_trans != NULL && PyDict_Check(blob_trans)) { \
        BlobMode _throwaway_mode; \
        boolean _throwaway_treat_subtype_text_as_text; \
        if (   validate_nonstandard_blob_config_dict(blob_trans, \
                  &_throwaway_mode, &_throwaway_treat_subtype_text_as_text \
               ) \
            != DTT_BLOB_CONFIG_VALID \
           ) \
        { return NULL; } \
      } \
    } \
    \
    if (!direction_is_IN) { \
      PyObject *output_type_trans_return_type_dict = \
        PyObject_CallFunctionObjArgs( \
            py__make_output_translator_return_type_dict_from_trans_dict, \
            trans_dict, \
            NULL \
          ); \
      if (output_type_trans_return_type_dict == NULL) { return NULL; } \
      if (    output_type_trans_return_type_dict != Py_None \
          && !PyDict_Check(output_type_trans_return_type_dict) \
         ) \
      { \
        raise_exception(InternalError, "Return value of" \
            " py__make_output_translator_return_type_dict_from_trans_dict was" \
            " not a dict or None." \
          ); \
        Py_DECREF(output_type_trans_return_type_dict); \
        return NULL; \
      } \
      \
      Py_XDECREF(target->output_type_trans_return_type_dict); \
      /* If the new output_type_trans_return_type_dict is None or empty, \
       * set the target's corresponding member to NULL rather than recording \
       * the useless incoming value. \
       * Note that output_type_trans_return_type_dict might be empty when \
       * trans_dict is *not* empty, because when a translator is set to None \
       * (which indicates that kinterbasdb should return its internal \
       * representation of the value), output_type_trans_return_type_dict \
       * will not contain an entry for that translation key (instead, \
       * XSQLDA2Description will supply a default type). */ \
      if (DICT_IS_NONE_OR_EMPTY(output_type_trans_return_type_dict)) { \
        Py_DECREF(output_type_trans_return_type_dict); \
        target->output_type_trans_return_type_dict = NULL; \
      } else { \
        target->output_type_trans_return_type_dict = \
          output_type_trans_return_type_dict; \
      } \
      \
      { /* Flush cached description tuples, because we've changed the output \
         * type translators, and that might invalidate the description \
         * tuples. */ \
        if (type_name ## _clear_ps_description_tuples(target) != 0) { \
          return NULL; \
        } \
      } \
    } \
    \
    Py_XDECREF(target->member_name); /* Free old translation dict, if any. */ \
    if (DICT_IS_NONE_OR_EMPTY(trans_dict)) { \
      target->member_name = NULL; \
    } else { \
      /* Corresponding DECREF is executed in target's destructor or, if the \
       * DTT settings are changed again during the life of target, by the \
       * XDECREF above. */ \
      Py_INCREF(trans_dict); \
      target->member_name = trans_dict; \
    } \
    \
    Py_INCREF(Py_None); \
    return Py_None; \
  }
/* end of _make__type_trans_setter */

#define _make__type_trans_getter(func_name, member_name, \
    type_name, type_infra_name, is_for_cursor \
  ) \
  static PyObject *func_name(PyObject *self, PyObject *args) { \
    type_name *target; \
    if (is_for_cursor) { \
      target = (type_name *) self; \
    } else { \
      if (!PyArg_ParseTuple(args, "O!", &type_infra_name, &target)) { \
        return NULL; \
      } \
    } \
    \
    if (target->member_name != NULL) { \
      /* Copy the dict so that the type translation settings can't be \
       * modified except via a set_type_trans_* method. */ \
      return PyDict_Copy(target->member_name); \
    } else { \
      Py_INCREF(Py_None); \
      return Py_None; \
    } \
  }
/* end of _make__type_trans_getter */

/* Getters/setters for CConnection: */
/* Out: */
_make__type_trans_setter(FALSE,
    pyob_Connection_set_type_trans_out, type_trans_out,
    CConnection, ConnectionType, FALSE
  )
_make__type_trans_getter(
    pyob_Connection_get_type_trans_out, type_trans_out,
    CConnection, ConnectionType, FALSE
  )
/* In: */
_make__type_trans_setter(TRUE,
    pyob_Connection_set_type_trans_in, type_trans_in,
    CConnection, ConnectionType, FALSE
  )
_make__type_trans_getter(
    pyob_Connection_get_type_trans_in, type_trans_in,
    CConnection, ConnectionType, FALSE
  )

/* Getters/setters for Cursor: */
/* Out: */
_make__type_trans_setter(FALSE,
    pyob_Cursor_set_type_trans_out, type_trans_out, Cursor, CursorType, TRUE
  )
_make__type_trans_getter(
    pyob_Cursor_get_type_trans_out, type_trans_out, Cursor, CursorType, TRUE
  )
/* In: */
_make__type_trans_setter(TRUE,
    pyob_Cursor_set_type_trans_in, type_trans_in, Cursor, CursorType, TRUE
  )
_make__type_trans_getter(
    pyob_Cursor_get_type_trans_in, type_trans_in, Cursor, CursorType, TRUE
  )


static PyObject *dynamically_type_convert_input_obj_if_necessary(
    PyObject *py_input,
    boolean is_array_element,
    unsigned short dialect,
    short data_type, short data_subtype, short scale,
    PyObject *converter
  )
{
  /* if $converter is None, returns:
   *   a new reference to the original py_input
   * else:
   *   the return value of the converter (which is a new reference) */
  assert (py_input != NULL);

  if (converter == Py_None) {
    Py_INCREF(py_input);
    return py_input;
  }{
  boolean is_fixed_point;
  PyObject *py_converted = NULL;
  PyObject *py_argument_to_converter;

  PyObject *argz = PyTuple_New(1);
  if (argz == NULL) { goto fail; }

  is_fixed_point = (
      is_array_element
        ? IS_FIXED_POINT__ARRAY_EL(dialect, data_type, data_subtype, scale)
        : IS_FIXED_POINT__CONVENTIONAL(dialect, data_type, data_subtype, scale)
    );

  /* Next, set py_argument_to_converter, the single argument that the converter
   * will receive (though it's only one argument, it might be a sequence). */

  /* Special case for fixed point fields:  pass the original input object and
   * the scale figure in a 2-tuple, rather than just the original input object,
   * as with most other field types. */
  if (is_fixed_point) {
    /* Reference ownership of this new 2-tuple is passed to argz via
     * PyTuple_SET_ITEM.  argz will then delete this new 2-tuple when argz
     * itself is deleted.  The refcount of py_input is INCd when it enters
     * the new 2-tuple; DECd when the 2-tuple is deleted. */
    py_argument_to_converter = Py_BuildValue("(Oi)", py_input, scale);
  } else if (IS_UNICODE_CHAR_OR_VARCHAR(data_type, data_subtype)) {
    py_argument_to_converter = Py_BuildValue("(Oi)", py_input, data_subtype);
  } else {
    /* We currently hold only a borrowed reference to py_input, since it's
     * an input parameter rather than a newly created object.
     * Therefore, we must now artificially INCREF py_input so that
     * PyTuple_SET_ITEM(argz, ...) can "steal" ownership of a reference to
     * py_input and then discard that reference when argz is destroyed. */
    Py_INCREF(py_input);
    py_argument_to_converter = py_input;
  }
  if (py_argument_to_converter == NULL) { goto fail; }

  PyTuple_SET_ITEM(argz, 0, py_argument_to_converter);

  py_converted = PyObject_CallObject(converter, argz); /* The MEAT. */
  if (py_converted == NULL) { goto fail; }

  /* Special case for fixed-point values with precision 10-18 in dialects < 3:
   * The value returned by the converter is a scaled Python int; we need to
   * convert it to a non-scaled Python float, because in dialects < 3,
   * logically fixed-point fields with precisions 10-18 are actually stored as
   * floating point.  Those with precisions 1-9 are stored internally as
   * integers, similar to the way they're stored in dialect 3; the clause
   *   && data_subtype == SUBTYPE_NONE
   * in the condition below prevents this special case from applying to the
   * precision 1-9 fields. */
  if (    dialect < 3 && is_fixed_point && scale != 0
       && data_subtype == SUBTYPE_NONE
       && py_converted != Py_None
     )
  {
    PyObject *py_conv_unscaled;
    PyObject *py_conv_as_py_float = PyNumber_Float(py_converted);
    if (py_conv_as_py_float == NULL) { goto fail; }
    py_conv_unscaled = PyFloat_FromDouble(
        PyFloat_AS_DOUBLE(py_conv_as_py_float) / pow(10.0f, (double) -scale)
      );
    Py_DECREF(py_conv_as_py_float);
    if (py_conv_unscaled == NULL) { goto fail; }
    /* Replace py_converted with py_conv_unscaled: */
    Py_DECREF(py_converted);
    py_converted = py_conv_unscaled;
  }

  goto cleanup;

  fail:
    assert (PyErr_Occurred());

    Py_XDECREF(py_converted);
    /* Fall through to cleanup. */

  cleanup:
    /* Notice that we do not DECREF py_argument_to_converter, because ownership
     * of the reference to py_argument_to_converter will have been pass to
     * the container argz. */
    Py_XDECREF(argz);

    return py_converted;
}} /* dynamically_type_convert_input_obj_if_necessary */


static PyObject *dynamically_type_convert_output_obj_if_necessary(
    PyObject *db_plain_output, PyObject *converter,
    short data_type, short data_subtype
  )
{
  /* Unlike dynamically_type_convert_input_obj_if_necessary, this function
   * does NOT return a new reference.
   * if converter is None:
   *   returns the passed reference to the original value db_plain_output
   * else:
   *   returns the return value of the converter (which is a new reference),
   *   BUT ALSO deletes the passed reference to db_plain_output, in effect
   *   "replacing" the db_plain_output reference with one to py_converted.
   *   The passed reference to db_plain_output is deleted EVEN if this function
   *   encounters an error. */
  assert (converter != NULL);
  /* If dealing with non-standard blob, this function never should've been
   * called: */
  assert (data_type == SQL_BLOB ? !PyDict_Check(converter) : TRUE);

  if (converter == Py_None) {
    return db_plain_output;
  } else {
    PyObject *py_converted;
    boolean is_unicode_char_or_varchar = IS_UNICODE_CHAR_OR_VARCHAR(
        data_type, data_subtype
      );
    PyObject *argz = PyTuple_New(1);
    if (argz == NULL) { goto fail; }

    if (!is_unicode_char_or_varchar) {
      /* The following statement "steals" the ref to db_plain_output, which is
       * appropriate behavior in this situation. */
      PyTuple_SET_ITEM(argz, 0, db_plain_output);
    } else {
      /* If it's a unicode CHAR or VARCHAR, create a 2-tuple containing:
       * (
       *    the raw (encoded) string,
       *    the database engine's internal character set code
       * ). */
      PyObject *db_charset_code;
      PyObject *tuple_of_raw_string_and_charset_code = PyTuple_New(2);
      if (tuple_of_raw_string_and_charset_code == NULL) { goto fail; }

      db_charset_code = PyInt_FromLong(data_subtype);
      if (db_charset_code == NULL) {
        Py_DECREF(tuple_of_raw_string_and_charset_code);
        goto fail;
      }

      /* The following statements "steal" the refs to the element values, which
       * is appropriate behavior in this situation.  Reference ownership of
       * db_plain_output and db_charset_code is handed off to the container
       * tuple_of_raw_string_and_charset_code; in turn, reference ownership of
       * tuple_of_raw_string_and_charset_code is handed off to the container
       * argz.
       * When argz is released at the end of this function, the release
       * "cascades", releasing the three other references mentioned above. */
      PyTuple_SET_ITEM(tuple_of_raw_string_and_charset_code, 0, db_plain_output);
      PyTuple_SET_ITEM(tuple_of_raw_string_and_charset_code, 1, db_charset_code);

      PyTuple_SET_ITEM(argz, 0, tuple_of_raw_string_and_charset_code);
    }

    py_converted = PyObject_CallObject(converter, argz); /* The MEAT. */

    Py_DECREF(argz);

    return py_converted;

    fail:
      assert (PyErr_Occurred());

      /* Yes, DECing db_plain_output here is correct (see comment at start): */
      Py_DECREF(db_plain_output);
      Py_XDECREF(argz);

      return NULL;
  }
} /* dynamically_type_convert_output_obj_if_necessary */
