#coding: utf-8
import pandas as pd
import datetime
import MySQLdb
from MySQLdb.cursors import Cursor
from typing import Any,Optional,Callable,Union,List,Tuple,Dict
from ..utils._colorings import toBLUE, toGREEN, toRED
from ..utils.environ_utils import check_environ
from ..utils.generic_utils import handleKeyError
from ._base import PycharmersAPI
[docs]class PycharmersMySQL(PycharmersAPI):
    """Wrapper class for MySQL.
    Args:
        host (Optional[str], optional)     : host to connect. Defaults to ``None``.
        password (Optional[str], optional) : user to connect as. Defaults to ``None``.
        user (Optional[str], optional)     : password to use. Defaults to ``None``.
        database (Optional[str], optional) : database to ues. Defaults to ``None``.
        verbose (bool, optional)           : Whether to print message or not Defaults to ``False``.
    """
    def __init__(self, host:Optional[str]=None, password:Optional[str]=None, user:Optional[str]=None, database:Optional[str]=None, verbose:bool=False):
        super().__init__(
            api_name="MySQL", 
            verbose=verbose,
            host=host, password=password, user=user, database=database,
        )
    
[docs]    def connect(self, func:Callable, host:Optional[str]=None, password:Optional[str]=None, user:Optional[str]=None, database:Optional[str]=None, **kwargs) -> Union[pd.DataFrame, Tuple[tuple], None]:
        """Use ``MySQLdb.connect`` to create a connection to the database, and close it after excuting ``func`` .
        Args:
            func (Callable)                    : The function you want to execute. Receive ``cursor`` as the first argument.
            host (Optional[str], optional)     : host to connect. Defaults to ``None``.
            password (Optional[str], optional) : user to connect as. Defaults to ``None``.
            user (Optional[str], optional)     : password to use. Defaults to ``None``.
            database (Optional[str], optional) : database to ues. Defaults to ``None``.
            \*\*kwargs (dict)                     : See a table below.
        Returns:
            Any: Return value of ``func``
        Keyword arguments for ``MySQLdb.connect`` is below.
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        |          Name          |   Type   |                                                                                                                                      Description                                                                                                                                       |
        +========================+==========+========================================================================================================================================================================================================================================================================================+
        | ``port``               | ``int``  | TCP/IP port to connect to                                                                                                                                                                                                                                                              |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``unix_socket``        | ``str``  | location of unix_socket to use                                                                                                                                                                                                                                                         |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``conv``               | ``dict`` | conversion dictionary, see MySQLdb.converters                                                                                                                                                                                                                                          |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``connect_timeout``    | ``int``  | number of seconds to wait before the connection attempt fails.                                                                                                                                                                                                                         |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``compress``           | ``bool`` | if set, compression is enabled                                                                                                                                                                                                                                                         |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``named_pipe``         | ``str``  | if set, a named pipe is used to connect (Windows only)                                                                                                                                                                                                                                 |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``init_command``       | ``str``  | command which is run once the connection is created                                                                                                                                                                                                                                    |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``read_default_file``  | ``str``  | file from which default client values are read                                                                                                                                                                                                                                         |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``read_default_group`` | ``str``  | configuration group to use from the default file                                                                                                                                                                                                                                       |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``cursorclass``        | ``type`` | class object, used to create cursors (keyword only)                                                                                                                                                                                                                                    |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``use_unicode``        | ``bool`` | If ``True``, text-like columns are returned as unicode objects using the connection``'s character set. Otherwise, text-like columns are returned as bytes. Unicode objects will always be encoded to the connection'``s character set regardless of this setting. Default to ``True``. |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``charset``            | ``str``  | If supplied, the connection character set will be changed to this character set.                                                                                                                                                                                                       |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``auth_plugin``        | ``str``  | If supplied, the connection default authentication plugin will be changed to this value. Example values are ``mysql_native_password`` or ``caching_sha2_password``                                                                                                                     |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``sql_mode``           | ``str``  | If supplied, the session SQL mode will be changed to this setting. For more details and legal values, see the MySQL documentation.                                                                                                                                                     |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``client_flag``        | ``int``  | flags to use or 0 (see MySQL docs or constants/CLIENTS.py)                                                                                                                                                                                                                             |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``ssl_mode``           | ``str``  | specify the security settings for connection to the server; see the MySQL documentation for more details (mysql_option(), MYSQL_OPT_SSL_MODE). Only one of ``'DISABLED'``, ``'PREFERRED'``, ``'REQUIRED'``, ``'VERIFY_CA'``, ``'VERIFY_IDENTITY'`` can be specified.                   |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``ssl``                | ``dict`` | dictionary or mapping contains SSL connection parameters; see the MySQL documentation for more details (mysql_ssl_set()).  If this is set, and the client does not support SSL, NotSupportedError will be raised.                                                                      |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``local_infile``       | ``bool`` | enables LOAD LOCAL INFILE; zero disables                                                                                                                                                                                                                                               |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``autocommit``         | ``bool`` | If False (default), autocommit is disabled. If ``True``, autocommit is enabled. If None, autocommit isn't set and server default is used.                                                                                                                                              |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        | ``binary_prefix``      | ``bool`` | If set, the ``'_binary'`` prefix will be used for raw byte query arguments (e.g. Binary). This is disabled by default.                                                                                                                                                                 |
        +------------------------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
        """
        check_environ(
            required_keynames=self.required_keynames, 
            required_env_varnames=self.required_env_varnames,
            host=host, password=password, user=user, database=database,
        )
        connection = MySQLdb.connect(
            host=self.get_val("host", host=host),
            password=self.get_val("password", password=password),
            user=self.get_val("user", user=user),
            database=self.get_val("database", database=database),
            use_unicode=True, charset="utf8", cursorclass=Cursor,
        )
        cursor = connection.cursor()
        ret = func(cursor=cursor, **kwargs)
        cursor.close()
        connection.commit()
        connection.close()
        return ret 
[docs]    def execute(self, query:str, columns:list=[], verbose:bool=False) -> Union[pd.DataFrame, Tuple[tuple], None]:
        """Execute a SQL query.
        Args:
            query (str)              : Query to execute on server.
            columns (list, optional) : If this value is fiven, Return value will be as a ``pd.DataFrame`` format. Defaults to ``[]``.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            Union[pd.DataFrame, Tuple[tuple], None]: Return value of query.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.execute("show databases;")
                (('information_schema',),
                 ('mysql',),
                 ('performance_schema',),
                 ('remody',),
                 ('sys',))
            >>> # Receive as a DataFrame.
            >>> df = sql.execute("show databases;", columns=["name"])
            >>> print(df.to_markdown())
                |    | name               |
                |---:|:-------------------|
                |  0 | information_schema |
                |  1 | mysql              |
                |  2 | performance_schema |
                |  3 | remody             |
                |  4 | sys                |
        """
        if verbose: print(query)
        def execute_query(cursor:Cursor) -> Tuple[tuple]:
            cursor.execute(query)
            return cursor.fetchall()
        ret = self.connect(func=execute_query)
        if len(columns)>0:
            ret = pd.DataFrame(data=ret, columns=columns)
        return ret 
[docs]    def create(self, table:str, verbose:bool=False, column_info={}) -> None:
        """Create a Table named ``table`` .
        Args:
            table (str)              : The name of table.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
            columninfo (dict)        : Key value style column information.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.create(table="pycharmers_user", column_info={
            ...     "id"      : 'int NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "ID"',
            ...     "username": 'VARCHAR(100) NOT NULL COMMENT "ユーザー名"',
            ...     "created" : 'datetime DEFAULT NULL COMMENT "登録日"' ,
            >>> })
        """
        return self.execute(f"CREATE TABLE {table} ({','.join([f'`{k}` {v}' for k,v in column_info.items()])});", verbose=verbose) 
[docs]    def drop(self, table:str, verbose:bool=False) -> None:
        """Drop the ``table`` .
        Args:
            table (str)              : The name of table.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.drop(table="pycharmers_user")
        """
        return self.execute(f"DROP TABLE {table};", verbose=verbose) 
[docs]    def describe(self, table:str, verbose:bool=False) -> pd.DataFrame:
        """Get table structure.
        Args:
            table (str)              : The name of table.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            pd.DataFrame: Table structure.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> df = sql.describe(table="pycharmers_user")
            >>> print(df.to_markdown())
            |    | Field    | Type         | Null   | Key   | Default   | Extra          |
            |---:|:---------|:-------------|:-------|:------|:----------|:---------------|
            |  0 | id       | int(11)      | NO     | PRI   |           | auto_increment |
            |  1 | username | varchar(100) | NO     |       |           |                |
            |  2 | created  | datetime     | YES    |       |           |                |
        """
        return self.execute(f"DESCRIBE {table};", columns=["Field","Type","Null","Key","Default","Extra"], verbose=verbose) 
[docs]    def insert(self, table:str, data:List[list], columns:list=[], col_type="input_field", verbose:bool=False) -> int:
        """Insert a record to specified ``table``
        Args:
            table (str)              : The name of the table.
            data (List[list])        : Data to be inserted.
            columns (list, optional) : Column names to be inserted values. Defaults to ``[]``.
            col_type (str, optional) : If ``columns`` has no data, use :meth:`get_colnames <pycharmers.api.mysql.PycharmersMySQL.get_colnames>` to decide which columns to be inseted. Defaults to ``"input_field"``.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            int: The number of rows in ``table``
        Examples:
            >>> import datetime
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.insert(table="pycharmers_user", data=[
            ...     ["iwasaki", "now()"],
            ...     ["shuto", datetime.datetime(1998,7,3)]
            >>> ], columns=["username", "created"])
        """
        if len(columns)==0:
            columns = self.get_colnames(table=table, col_type=col_type)
        if not isinstance(data[0], list):
            data = [data]
        df_field = self.describe(table=table)
        coltypes = df_field.set_index("Field").filter(items=columns, axis=0).Type
        values = ", ".join([f"({', '.join([self.format_data(e,t) for e,t in zip(d,coltypes)])})" for d in data])
        return self.execute(f"INSERT INTO {table} ({', '.join(columns)}) VALUES {values}", verbose=verbose) 
[docs]    def update(self, table:str, old_column:str, old_value:Any, new_columns:List[str], new_values:List[Any], verbose:bool=False) -> None:
        """Update records.
        Args:
            table (str)              : The name of table.
            old_column (str)         : [description]
            old_value (Any)          : 
            new_columns (List[str])  : [description]
            new_values (List[Any])   : 
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> print(sql.selectAll(table="pycharmers_user").to_markdown())
                |    |   id | username   | created             |
                |---:|-----:|:-----------|:--------------------|
                |  0 |    1 | iwasaki    | 2021-05-22 07:23:10 |
                |  1 |    2 | shuto      | 1998-07-03 00:00:00 |
            >>> sql.update(table="pycharmers_user", old_column="username", old_value="iwasaki", new_column="created", new_value="now()")
            >>> print(sql.selectAll(table="pycharmers_user").to_markdown())
                |    |   id | username   | created             |
                |---:|-----:|:-----------|:--------------------|
                |  0 |    1 | iwasaki    | 2021-05-22 07:28:09 |
                |  1 |    2 | shuto      | 1998-07-03 00:00:00 |
        """
        df_field = self.describe(table=table)
        old_type = df_field[df_field.Field==old_column].Type.values[0]
        new_coltypes = df_field.set_index("Field").filter(items=new_columns, axis=0).Type
        update_values = ", ".join([f"{col} = {self.format_data(val, type)}" for col,val,type in zip(new_columns, new_values, new_coltypes)])
        return self.execute(f"UPDATE {table} SET {update_values} WHERE {old_column} = {self.format_data(old_value, old_type)}", verbose=verbose) 
[docs]    def delete(self, table:str, column:str, value:Any, verbose:bool=False) -> None:
        """Delete the records whose ``column`` is ``value`` from the table named ``table`` .
        Args:
            table (str)              : The name of table.
            column (str)             : 
            value (Any)              : 
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> print(sql.select(table="pycharmers_user").to_markdown())
                |    |   id | username   | created             |
                |---:|-----:|:-----------|:--------------------|
                |  0 |    1 | iwasaki    | 2021-05-22 07:23:10 |
                |  1 |    2 | shuto      | 1998-07-03 00:00:00 |
            >>> sql.delete(table="pycharmers_user", column="username", value="shuto")
            >>> print(sql.select(table="pycharmers_user").to_markdown())
                |    |   id | username   | created             |
                |---:|-----:|:-----------|:--------------------|
                |  0 |    1 | iwasaki    | 2021-05-22 07:23:10 |
        """
        df_field = self.describe(table=table)
        type = df_field[df_field.Field==column].Type.values[0]
        return self.execute(f"DELETE FROM {table} WHERE {column} = {self.format_data(value, type)}", verbose=verbose) 
[docs]    def merge(self, table1info:Dict[str,List[str]], table2info:Dict[str,List[str]], method:str="inner", verbose:bool=False) -> pd.DataFrame:
        """Select values from table1 and table2 using ``method`` JOIN
        Args:
            table1info (Dict[str,List[str]]) : Table1 information ( ``key`` : table name, ``value`` : selected columns (0th column is used for merging.))
            table2info (Dict[str,List[str]]) : Table2 information ( ``key`` : table name, ``value`` : selected columns (0th column is used for merging.))
            method (str, optional)           : How to merge two tables. Defaults to ``"inner"``.
            verbose (bool, optional)         : Whether to display the query or not. Defaults to ``False``.
        Returns:
            pd.DataFrame: Merged table records.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql.merge(
            ...     table1info={"user": ["id", "name", "job_id"]}, 
            ...     table2info={"jobs": ["id", "job_name"]}, 
            ...     method="inner", verbose=True
            >>> )
            >>> SELECT user.name, user.job_id, jobs.job_name FROM user INNER JOIN jobs ON user.id = jobs.id;
        """
        table1,columns1 = table1info.popitem()
        table2,columns2 = table2info.popitem()
        col_merge1 = columns1.pop(0)
        col_merge2 = columns2.pop(0)
        return self.execute(f"SELECT {', '.join([f'{table1}.{col}' for col in columns1] + [f'{table2}.{col}' for col in columns2])} FROM {table1} {method.upper()} JOIN {table2} ON {table1}.{col_merge1} = {table2}.{col_merge2};", columns=columns1+columns2, verbose=verbose) 
[docs]    def get_colnames(self, table:str, col_type:str="all") -> list:
        """Get column names in the specified ``table`` .
        Args:
            table (str)              : The name of table.
            col_type (str, optional) : What types columns to extract. Defaults to ``"all"``.
        Returns:
            list: Extracted Columns.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.get_colnames(table="pycharmers_user")
            >>> ['id', 'username', 'created']
        """
        handleKeyError(lst=["all", "minimum", "input_field"], col_type=col_type)
        df = self.describe(table=table)
        if col_type=="all":
            pass
        elif col_type=="minimum":
            df = df[(df.Extra!="auto_increment") & (df.Null=="NO")]
        elif col_type=="input_field":
            df = df[df.Extra!="auto_increment"]
        return df.Field.to_list() 
[docs]    def select(self, table:str, columns:List[str]=[], where:Dict[str,Any]={}, verbose:bool=False) -> pd.DataFrame:
        """Get selected records.
        Args:
            table (str)                     : The name of table.
            columns (List[str], optional)   : Selected Columns. If you don't specify any columns, extract all columns. Defaults to ``[]``.
            where (Dict[str,Any], optional) : Specify the condition of the column to be extracted. { ``colname`` : ``value`` }
            verbose (bool, optional)        : Whether to display the query or not. Defaults to ``False``.
        Returns:
            pd.DataFrame: Selected Records.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> df = sql.select(table="pycharmers_user")
            >>> print(df.to_markdown())
                |    |   id | username   | created             |
                |---:|-----:|:-----------|:--------------------|
                |  0 |    1 | iwasaki    | 2021-05-22 07:23:10 |
                |  1 |    2 | shuto      | 1998-07-03 00:00:00 |
            >>> df = sql.select(table="pycharmers_user", columns=["id", "username"])
            >>> print(df.to_markdown())
                |    |   id | username   |
                |---:|-----:|:-----------|
                |  0 |    1 | iwasaki    |
                |  1 |    2 | shuto      |
        """
        if len(columns)==0:
            columns = self.get_colnames(table=table, col_type="all")
        if len(where)>0:
            df_field = self.describe(table=table)
            coltypes = df_field.set_index("Field").filter(items=list(where.keys()), axis=0).Type
            WHERE = f'WHERE {" AND ".join([f"{key} = {self.format_data(val,type)}" for type,(key,val) in zip(coltypes,where.items())])}'
        else:
            WHERE = ""
        return self.execute(f"SELECT {', '.join(columns)} FROM {table} {WHERE};", columns=columns, verbose=verbose) 
[docs]    def count_rows(self, table:str, verbose:bool=False) -> int:
        """Get the number of rows in the ``table`` .
        Args:
            table (str)              : The name of table.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            int: The number of rows in the specified ``table`` .
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> sql.count_rows(table="pycharmers_user")
                2
        """
        return self.execute(f"SELECT count(*) FROM {table};")[0][0] 
[docs]    def show_tables(self, verbose:bool=False) -> pd.DataFrame:
        """Show all tables.
        Args:
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            pd.DataFrame: Information for all Tables.
        >>> from pycharmers.api import PycharmersMySQL
        >>> sql = PycharmersMySQL()
        >>> df = sql.show_tables()
        >>> print(df.to_markdown())
            |    | Name            | Engine   |   Version | Row_format   |   Rows |   Avg_row_length |   Data_length |   Max_data_length |   Index_length |   Data_free |   Auto_increment | Create_time         | Update_time         | Check_time   | Collation       | Checksum   | Create_options   | Comment   |
            |---:|:----------------|:---------|----------:|:-------------|-------:|-----------------:|--------------:|------------------:|---------------:|------------:|-----------------:|:--------------------|:--------------------|:-------------|:----------------|:-----------|:-----------------|:----------|
            |  0 | pycharmers_user | InnoDB   |        10 | Dynamic      |      0 |                0 |         16384 |                 0 |              0 |           0 |                1 | 2021-05-22 07:06:17 | NaT                 |              | utf8_general_ci |            |                  |           |
        """
        return self.execute("show table status;", columns=['Name', 'Engine', 'Version', 'Row_format', 'Rows', 'Avg_row_length', 'Data_length', 'Max_data_length', 'Index_length', 'Data_free', 'Auto_increment', 'Create_time', 'Update_time', 'Check_time', 'Collation', 'Checksum', 'Create_options', 'Comment'], verbose=verbose) 
[docs]    def explain(self, table:str, verbose:bool=False) -> pd.DataFrame:
        """Explain records
        Args:
            table (str)              : The name of table.
            verbose (bool, optional) : Whether to display the query or not. Defaults to ``False``.
        Returns:
            pd.DataFrame: Explanation for records.
        Examples:
            >>> from pycharmers.api import PycharmersMySQL
            >>> sql = PycharmersMySQL()
            >>> df = sql.explain(table="pycharmers_user")
            >>> print(df.to_markdown())
            |    |   id | select_type   | table           | partition   | type   | possible_keys   | key   | key_len   | ref   |   rows |   filtered | Extra   |
            |---:|-----:|:--------------|:----------------|:------------|:-------|:----------------|:------|:----------|:------|-------:|-----------:|:--------|
            |  0 |    1 | SIMPLE        | pycharmers_user |             | ALL    |                 |       |           |       |      2 |        100 |         |
        """
        return self.execute(f"EXPLAIN SELECT * FROM {table};", columns=["id","select_type","table","partition","type","possible_keys","key","key_len","ref","rows","filtered","Extra"], verbose=verbose)