Convert the result of a SQL database query to an SFrame.

        Parameters
        ----------
        conn : dbapi2.Connection
          A DBAPI2 connection object. Any connection object originating from
          the 'connect' method of a DBAPI2-compliant package can be used.

        sql_statement : str
          The query to be sent to the database through the given connection.
          No checks are performed on the `sql_statement`. Any side effects from
          the query will be reflected on the database.  If no result rows are
          returned, an empty SFrame is created.

        params : iterable | dict, optional
          Parameters to substitute for any parameter markers in the
          `sql_statement`. Be aware that the style of parameters may vary
          between different DBAPI2 packages.

        type_inference_rows : int, optional
          The maximum number of rows to use for determining the column types of
          the SFrame. These rows are held in Python until all column types are
          determined or the maximum is reached.

        dbapi_module : module | package, optional
          The top-level DBAPI2 module/package that constructed the given
          connection object. By default, a best guess of which module the
          connection came from is made. In the event that this guess is wrong,
          this will need to be specified.

        column_type_hints : dict | list | type, optional
          Specifies the types of the output SFrame. If a dict is given, it must
          have result column names as keys, but need not have all of the result
          column names. If a list is given, the length of the list must match
          the number of result columns. If a single type is given, all columns
          in the output SFrame will be this type. If the result type is
          incompatible with the types given in this argument, a casting error
          will occur.

        cursor_arraysize : int, optional
          The number of rows to fetch from the database at one time.

        Returns
        -------
        out : SFrame

        Examples
        --------
        >>> import sqlite3

        >>> conn = sqlite3.connect('example.db')

        >>> turicreate.SFrame.from_sql(conn, "SELECT * FROM foo")
        Columns:
                a       int
                b       int
        Rows: 1
        Data:
        +---+---+
        | a | b |
        +---+---+
        | 1 | 2 |
        +---+---+
        [1 rows x 2 columns]

def from_sql(cls, conn, sql_statement, params=None, type_inference_rows=100,
        dbapi_module=None, column_type_hints=None, cursor_arraysize=128):
        """
        Convert the result of a SQL database query to an SFrame.

        Parameters
        ----------
        conn : dbapi2.Connection
          A DBAPI2 connection object. Any connection object originating from
          the 'connect' method of a DBAPI2-compliant package can be used.

        sql_statement : str
          The query to be sent to the database through the given connection.
          No checks are performed on the `sql_statement`. Any side effects from
          the query will be reflected on the database.  If no result rows are
          returned, an empty SFrame is created.

        params : iterable | dict, optional
          Parameters to substitute for any parameter markers in the
          `sql_statement`. Be aware that the style of parameters may vary
          between different DBAPI2 packages.

        type_inference_rows : int, optional
          The maximum number of rows to use for determining the column types of
          the SFrame. These rows are held in Python until all column types are
          determined or the maximum is reached.

        dbapi_module : module | package, optional
          The top-level DBAPI2 module/package that constructed the given
          connection object. By default, a best guess of which module the
          connection came from is made. In the event that this guess is wrong,
          this will need to be specified.

        column_type_hints : dict | list | type, optional
          Specifies the types of the output SFrame. If a dict is given, it must
          have result column names as keys, but need not have all of the result
          column names. If a list is given, the length of the list must match
          the number of result columns. If a single type is given, all columns
          in the output SFrame will be this type. If the result type is
          incompatible with the types given in this argument, a casting error
          will occur.

        cursor_arraysize : int, optional
          The number of rows to fetch from the database at one time.

        Returns
        -------
        out : SFrame

        Examples
        --------
        >>> import sqlite3

        >>> conn = sqlite3.connect('example.db')

        >>> turicreate.SFrame.from_sql(conn, "SELECT * FROM foo")
        Columns:
                a       int
                b       int
        Rows: 1
        Data:
        +---+---+
        | a | b |
        +---+---+
        | 1 | 2 |
        +---+---+
        [1 rows x 2 columns]
        """
        # Mapping types is always the trickiest part about reading from a
        # database, so the main complexity of this function concerns types.
        # Much of the heavy-lifting of this is done by the DBAPI2 module, which
        # holds the burden of the actual mapping from the database-specific
        # type to a suitable Python type. The problem is that the type that the
        # module chooses may not be supported by SFrame, and SFrame needs a
        # list of types to be created, so we must resort to guessing the type
        # of a column if the query result returns lots of NULL values. The goal
        # of these steps is to fail as little as possible first, and then
        # preserve data as much as we can.
        #
        # Here is how the type for an SFrame column is chosen:
        #
        # 1. The column_type_hints parameter is checked.
        #
        #    Each column specified in the parameter will be forced to the
        #    hinted type via a Python-side cast before it is given to the
        #    SFrame. Only int, float, and str are allowed to be hints.
        #
        # 2. The types returned from the cursor are checked.
        #
        #    The first non-None result for each column is taken to be the type
        #    of that column. The type is checked for whether SFrame supports
        #    it, or whether it can convert to a supported type. If the type is
        #    supported, no Python-side cast takes place. If unsupported, the
        #    SFrame column is set to str and the values are casted in Python to
        #    str before being added to the SFrame.
        #
        # 3. DB type codes provided by module are checked
        #
        #    This case happens for any column that only had None values in the
        #    first `type_inference_rows` rows. In this case we check the
        #    type_code in the cursor description for the columns missing types.
        #    These types often do not match up with an SFrame-supported Python
        #    type, so the utility of this step is limited. It can only result
        #    in labeling datetime.datetime, float, or str. If a suitable
        #    mapping isn't found, we fall back to str.
        mod_info = _get_global_dbapi_info(dbapi_module, conn)

        from .sframe_builder import SFrameBuilder

        c = conn.cursor()
        try:
            if params is None:
                c.execute(sql_statement)
            else:
                c.execute(sql_statement, params)
        except mod_info['Error'] as e:
            # The rollback method is considered optional by DBAPI2, but some
            # modules that do implement it won't work again unless it is called
            # if an error happens on a cursor.
            if hasattr(conn, 'rollback'):
                conn.rollback()
            raise e

        c.arraysize = cursor_arraysize

        result_desc = c.description
        result_names = [i[0] for i in result_desc]
        result_types = [None for i in result_desc]

        cols_to_force_cast = set()
        temp_vals = []

        # Set any types that are given to us
        col_name_to_num = {result_names[i]:i for i in range(len(result_names))}
        if column_type_hints is not None:
            if type(column_type_hints) is dict:
                for k,v in column_type_hints.items():
                    col_num = col_name_to_num[k]
                    cols_to_force_cast.add(col_num)
                    result_types[col_num] = v
            elif type(column_type_hints) is list:
                if len(column_type_hints) != len(result_names):
                    __LOGGER__.warn("If column_type_hints is specified as a "+\
                        "list, it must be of the same size as the result "+\
                        "set's number of columns. Ignoring (use dict instead).")
                else:
                    result_types = column_type_hints
                    cols_to_force_cast.update(range(len(result_desc)))
            elif type(column_type_hints) is type:
                result_types = [column_type_hints for i in result_desc]
                cols_to_force_cast.update(range(len(result_desc)))

        # Since we will be casting whatever we receive to the types given
        # before submitting the values to the SFrame, we need to make sure that
        # these are types that a "cast" makes sense, and we're not calling a
        # constructor that expects certain input (e.g.  datetime.datetime),
        # since we could get lots of different input
        hintable_types = [int,float,str]
        if not all([i in hintable_types or i is None for i in result_types]):
            raise TypeError("Only " + str(hintable_types) + " can be provided as type hints!")

        # Perform type inference by checking to see what python types are
        # returned from the cursor
        if not all(result_types):
            # Only test the first fetch{one,many} command since the only way it
            # will raise an exception is if execute didn't produce a result set
            try:
                row = c.fetchone()
            except mod_info['Error'] as e:
                if hasattr(conn, 'rollback'):
                    conn.rollback()
                raise e
            while row is not None:
                # Assumes that things like dicts are not a "single sequence"
                temp_vals.append(row)
                val_count = 0
                for val in row:
                    if result_types[val_count] is None and val is not None:
                        result_types[val_count] = type(val)
                    val_count += 1
                if all(result_types) or len(temp_vals) >= type_inference_rows:
                    break
                row = c.fetchone()

        # This will be true if some columns have all missing values up to this
        # point. Try using DBAPI2 type_codes to pick a suitable type. If this
        # doesn't work, fall back to string.
        if not all(result_types):
            missing_val_cols = [i for i,v in enumerate(result_types) if v is None]
            cols_to_force_cast.update(missing_val_cols)
            inferred_types = _infer_dbapi2_types(c, mod_info)
            cnt = 0
            for i in result_types:
                if i is None:
                    result_types[cnt] = inferred_types[cnt]
                cnt += 1

        sb = SFrameBuilder(result_types, column_names=result_names)
        unsupported_cols = [i for i,v in enumerate(sb.column_types()) if v is type(None)]
        if len(unsupported_cols) > 0:
            cols_to_force_cast.update(unsupported_cols)
            for i in unsupported_cols:
                result_types[i] = str
            sb = SFrameBuilder(result_types, column_names=result_names)

        temp_vals = _convert_rows_to_builtin_seq(temp_vals)
        sb.append_multiple(_force_cast_sql_types(temp_vals, result_types, cols_to_force_cast))
        rows = c.fetchmany()
        while len(rows) > 0:
            rows = _convert_rows_to_builtin_seq(rows)
            sb.append_multiple(_force_cast_sql_types(rows, result_types, cols_to_force_cast))
            rows = c.fetchmany()
        cls = sb.close()

        try:
            c.close()
        except mod_info['Error'] as e:
            if hasattr(conn, 'rollback'):
                conn.rollback()
            raise e
        return cls