/* KInterbasDB Python Package - Implementation of Field Precision Determination
 *
 * 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. */

#define ENTITY_TYPE_UNKNOWN 0
#define ENTITY_TYPE_TABLE 1
#define ENTITY_TYPE_STORED_PROCEDURE 2

#define ENTITY_TYPE_LAST ENTITY_TYPE_STORED_PROCEDURE

static PyObject *determine_field_precision(
    short entity_type_code,
    char *entity_name, short entity_name_length,
    char *field_name, short field_name_length,
    Cursor *cursor
  )
{
  /* Returns:
   * - a new reference to a PyObject * containing the precision figure on
   *   success (may be the PyInt zero)
   * - a new reference to a PyObject * containing the PyInt zero on routine
   *   inability to determine precision (as in the case of dynamic fields)
   * - NULL on error (will already have set an exception) */
  int status = -1;
  PyObject *precision = NULL;
  PyObject *result_cache = NULL;
  PyObject *result_cache_this_entity = NULL;

  const char *sql_statement_table =
       "SELECT FIELD_SPEC.RDB$FIELD_PRECISION"
      " 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 = ?"
    ;
  const unsigned short sql_statement_table_length = (unsigned short)
    strlen(sql_statement_table);

  const char *sql_statement_stored_procedure =
       "SELECT FIELD_SPEC.RDB$FIELD_PRECISION"
      " FROM RDB$FIELDS FIELD_SPEC, RDB$PROCEDURE_PARAMETERS REL_FIELDS"
      " WHERE"
            " FIELD_SPEC.RDB$FIELD_NAME = REL_FIELDS.RDB$FIELD_SOURCE"
        " AND RDB$PROCEDURE_NAME = ?"
        " AND RDB$PARAMETER_NAME = ?"
        " AND RDB$PARAMETER_TYPE = 1" /* 1 is the parameter type of output
                                       * parameters */
    ;
  const unsigned short sql_statement_stored_procedure_length = (unsigned short)
    strlen(sql_statement_stored_procedure);

  /* The following variables are all just shortcuts to members of
   * result_cache: */
  XSQLDA *in_da = NULL;
  XSQLVAR *in_var = NULL;
  XSQLDA *out_da = NULL;
  XSQLVAR *out_var = NULL;

  CursorDescriptionCache *cache =
    Transaction_get_con(cursor->trans)->desc_cache;
  PyObject *exception_type = NULL;

  /* Default to normal table. */
  if (entity_type_code == ENTITY_TYPE_UNKNOWN) {
    entity_type_code = ENTITY_TYPE_TABLE;
  }

  if (entity_name_length == 0 || field_name_length == 0) {
    /* Either or both of the entity name and the field name are not supplied,
     * so we cannot determine this output field's precision.  This is not
     * an exceptional situation; it occurs routinely in queries with
     * dynamically computed fields (e.g., select count(*) from some_table). */
    return PyInt_FromLong(0);
  }

  /* Special case for the automagic RDB$DB_KEY field, which the engine isn't
   * able to find the precision of.  The engine mangles the field name to
   * "DB_KEY" instead of "RDB$DB_KEY", but I'm testing for either here in the
   * interest of future-proofing. */
  if (
         (field_name_length ==  6 && strncmp(field_name, "DB_KEY",      6) == 0)
      || (field_name_length == 10 && strncmp(field_name, "RDB$DB_KEY", 10) == 0)
     )
  { return PyInt_FromLong(0); }

  /* If the cache has not yet been allocated and prepared, do so now.
   * If it has already been allocated, just set some local "shortcut pointers"
   * and proceed directly to the query execution. */

  if (cache != NULL) {
    /* If the precison figure for this entity.field is already cached, just
     * retrieve it from the cache dictionary and return. */
    result_cache = cache->result_cache;
    assert (result_cache != NULL);
    result_cache_this_entity =
      PyDict_GetItemString(result_cache, entity_name); /* borrowed ref */

    if (result_cache_this_entity != NULL) {
      precision = PyDict_GetItemString(result_cache_this_entity, field_name);

      if (precision != NULL) {
        /* Cache hit.
         * PyDict_GetItemString returned borrowed ref, so we need to INCREF. */
        Py_INCREF(precision);
        return precision;
      }
    } else {
      /* There is not even a cache for this entity, so there cannot possibly be
       * one for this entity.field.  Create a new dictionary to hold the cached
       * precision figures for this entity. */
      result_cache_this_entity = PyDict_New();
      if (result_cache_this_entity == NULL) { goto fail; }

      status = PyDict_SetItemString(result_cache,
          entity_name, result_cache_this_entity
        );
      /* PyDict_SetItemString established its own ref. */
      Py_DECREF(result_cache_this_entity);
      if (status == -1) { goto fail; }
    }
    /* The precision figure was not cached; fall through and query the system
     * tables. */

    in_da = cache->in_da;
    out_da = cache->out_da;
    out_var = cache->out_da->sqlvar;
  } else {
    /* cache itself was NULL, so we're starting from scratch by allocating the
     * cache structure.  Won't need to explicitly deallocate it on error,
     * because CConnection's field cleanup code will take care of that. */
    cache = Transaction_get_con(cursor->trans)->desc_cache = kimem_main_malloc(
        sizeof(CursorDescriptionCache)
      );
    if (cache == NULL) { goto fail; }

    cache->in_da = (XSQLDA *) cache->in_da_mem;

    cache->out_da = (XSQLDA *) cache->out_da_mem;
    cache->out_var_sqldata = -1;
    cache->out_var_sqlind = SQLIND_NULL;
    out_var = cache->out_da->sqlvar;
    out_var->sqldata = (char *) &cache->out_var_sqldata;
    out_var->sqlind = &cache->out_var_sqlind;

    /* The dictionary result_cache will cache entity-specific dictionaries that
     * will contain the field precision figures that have been determined via
     * queries to system tables.
     * Notice that we attach result_cache to the CursorDescriptionCache object,
     * which is tracked by the CConnection, so on error, deallocation of
     * result_cache will be handled implicitly by the CConnection's
     * field cleanup code. */
    result_cache = cache->result_cache = PyDict_New();
    if (result_cache == NULL) { goto fail; }

    /* There was no cache at all, so there could not have been a cache for this
     * entity.  Create one. */
    result_cache_this_entity = PyDict_New();
    if (result_cache_this_entity == NULL) { goto fail; }

    status = PyDict_SetItemString(result_cache,
        entity_name, result_cache_this_entity
      );
    Py_DECREF(result_cache_this_entity);
    if (status == -1) { goto fail; }

    /* Set up the query output structures.  We know at design time exactly how
     * they should be configured, so there's no convoluted dynamism here, as
     * there is in servicing an arbitrary query that originated in Python
     * client code. */
    out_da = cache->out_da;
    out_da->version = SQLDA_VERSION_KIDB;
    out_da->sqln = 1;

    /* Set up the input structures (again, their configuration is mostly
     * static). */
    in_da = cache->in_da;
    in_da->version = SQLDA_VERSION_KIDB;
    in_da->sqln = 2;
    in_da->sqld = 2;

    /* Set the type flags of the input variables (they never change): */
    in_da->sqlvar      ->sqltype = SQL_TEXT;
    (in_da->sqlvar + 1)->sqltype = SQL_TEXT;

    /* Allocate the statement structures.  MUST set statement handles to NULL
     * before calls to isc_dsql_allocate_statement. */
    ENTER_GDAL

    cache->stmt_handle_table = NULL_STMT_HANDLE;
    cache->stmt_handle_stored_procedure = NULL_STMT_HANDLE;

    isc_dsql_allocate_statement(cursor->status_vector,
        Transaction_get_db_handle_p(cursor->trans),
        &cache->stmt_handle_table
      );
    if (DB_API_ERROR(cursor->status_vector)) {
      LEAVE_GDAL_WITHOUT_ENDING_CODE_BLOCK
      goto fail_with_operationalerror;
    }

    isc_dsql_allocate_statement(cursor->status_vector,
        Transaction_get_db_handle_p(cursor->trans),
        &cache->stmt_handle_stored_procedure
      );
    LEAVE_GDAL

    if (DB_API_ERROR(cursor->status_vector)) {
      goto fail_with_operationalerror;
    }

    /* Prepare the statements. */
    {
      isc_tr_handle *trans_handle_addr =
        Transaction_get_handle_p(cursor->trans);

      ENTER_GDAL
      isc_dsql_prepare(cursor->status_vector,
          trans_handle_addr,
          &cache->stmt_handle_table,
          sql_statement_table_length,
          (char *) sql_statement_table,
          Transaction_get_dialect(cursor->trans),
          out_da
        );

      if (DB_API_ERROR(cursor->status_vector)) {
        LEAVE_GDAL_WITHOUT_ENDING_CODE_BLOCK
        goto fail_with_operationalerror;
      }

      isc_dsql_prepare(cursor->status_vector,
          trans_handle_addr,
          &cache->stmt_handle_stored_procedure,
          sql_statement_stored_procedure_length,
          (char *) sql_statement_stored_procedure,
          Transaction_get_dialect(cursor->trans),
          out_da
        );
      LEAVE_GDAL

      if (DB_API_ERROR(cursor->status_vector)) {
        goto fail_with_operationalerror;
      }
    }
  }

  /* We are now done (allocating and preparing new)/(loading references to
   * existing) description cache structures. */
  assert (in_da != NULL);
  assert (out_da != NULL);
  assert (out_var != NULL);
  assert (out_var == out_da->sqlvar);

  /* Set the names of the relation and field for which we're determining
   * precision. */
  in_var = in_da->sqlvar; /* First input variable. */
  assert (in_var->sqltype == SQL_TEXT);
  in_var->sqllen = entity_name_length;
  in_var->sqldata = entity_name;

  in_var++; /* Second input variable. */
  assert (in_var->sqltype == SQL_TEXT);
  in_var->sqllen = field_name_length;
  in_var->sqldata = field_name;

  /* Execute the prepared statement. */
  switch (entity_type_code) {
    case ENTITY_TYPE_TABLE:
      {
        isc_tr_handle *trans_handle_addr =
          Transaction_get_handle_p(cursor->trans);

        ENTER_GDAL
        isc_dsql_execute2(cursor->status_vector,
            trans_handle_addr,
            &cache->stmt_handle_table,
            Transaction_get_dialect(cursor->trans),
            in_da,
            out_da
          );
        LEAVE_GDAL
      }
      break;

    case ENTITY_TYPE_STORED_PROCEDURE:
      {
        isc_tr_handle *trans_handle_addr =
          Transaction_get_handle_p(cursor->trans);

        ENTER_GDAL
        isc_dsql_execute2(cursor->status_vector,
            trans_handle_addr,
            &cache->stmt_handle_stored_procedure,
            Transaction_get_dialect(cursor->trans),
            in_da,
            out_da
          );
        LEAVE_GDAL
      }
      break;

    default:
      raise_exception(InternalError, "determine_field_precision called with"
        " invalid entity type directive.");
      goto fail;
  }

  if (DB_API_ERROR(cursor->status_vector)) {
    /* If we've recursed as far as possible, yet have run out of entity types
     * to try, we must give up and raise an error. */
    if (entity_type_code == ENTITY_TYPE_LAST) {
      exception_type = InternalError;
      goto fail;
    } else {
      /* Recursively try the next alternative entity type. */
      precision = determine_field_precision(
          (short) (entity_type_code + 1),
          entity_name, entity_name_length,
          field_name, field_name_length,
          cursor
        );
      if (precision == NULL) { goto fail; }
    }
  } else {
    Transaction_stats_record_ps_executed(cursor->trans);

    /* Both PyInt_FromLong and PyDict_SetItemString create new references, so
     * although we want to store one reference to the int object and return
     * another reference to the same object, there's no need to
     *   INCREF(precision)
     * here. */
    precision = PyInt_FromLong(
          cache->out_var_sqlind == SQLIND_NULL ? 0 : cache->out_var_sqldata
      );
    if (precision == NULL) { goto fail; }

    /* Cache the precision figure. */
    if (PyDict_SetItemString(result_cache_this_entity, field_name, precision)
        == -1
       )
    {
      Py_DECREF(precision);
      goto fail;
    }
  }

  assert (precision != NULL);
  assert (PyInt_CheckExact(precision));
  return precision;

  fail_with_operationalerror:
    exception_type = OperationalError;
    /* Fall through to fail: */
  fail:
    /* If no exception_type was specified, there should already be a Python
     * exception set. */
    if (exception_type == NULL) {
      assert (PyErr_Occurred());
    } else {
      raise_sql_exception(exception_type,
          "Unable to determine field precison from system tables: ",
          cursor->status_vector
        );
    }

    return NULL;
} /* determine_field_precision */

static void free_field_precision_cache(
    CursorDescriptionCache *cache, boolean should_try_to_free_stmt_handles,
    ISC_STATUS *status_vector
  )
{
  if (cache == NULL) { return; }

  /* Added should_try_to_free_stmt_handles flag so that the connection can
   * prevent this function from calling isc_dsql_free_statement if the
   * connection knows that it has lost its database handle (in some versions of
   * the Firebird client library, isc_dsql_free_statement causes a segfault
   * when the connection under which the handles were established is no longer
   * valid). */
  if (!should_try_to_free_stmt_handles) {
    cache->stmt_handle_table = NULL_STMT_HANDLE;
    cache->stmt_handle_stored_procedure = NULL_STMT_HANDLE;
  } else {
    assert (cache->stmt_handle_table != NULL_STMT_HANDLE);
    assert (cache->stmt_handle_stored_procedure != NULL_STMT_HANDLE);

    ENTER_GDAL
    isc_dsql_free_statement(status_vector,
        &cache->stmt_handle_table,
        DSQL_drop
      );

    isc_dsql_free_statement(status_vector,
        &cache->stmt_handle_stored_procedure,
        DSQL_drop
      );
    LEAVE_GDAL
  }

  /* Free the master cache dictionary, which will of course free its
   * subordinate (entity-specific) dictionaries: */
  Py_XDECREF(cache->result_cache);

  /* Free the passed cache object itself: */
  kimem_main_free(cache);
} /* free_field_precision_cache */
