Repository: mindsdb/mindsdb

Issue:
[New Integration]:  Integration with Informix
### Is there an existing integration?

- [X] I have searched the existing integrations.

### Use Case

Informix is versatile database provided by IBM. 
It is used by many prominent company and also by many developers.
IBM Informix is an embeddable, high-performance database for integrating SQL, NoSQL, JSON, time-series and spatial data. Designed for analytics on the edge, in the cloud or on premises, the low-footprint database features self-management and automated administrative capabilities.

### Motivation

It would attract company and developer using Informix to use mindsDB. 
And it will be very effective to get large customer base from Informix. 

### Implementation

_No response_

### Anything else?

_No response_


Discussion:
### @ZoranPandovski 

**What do you think about this ?**

@ZoranPandovski let's talk about this integration 
Sure, let's add handler for it
**_Sure i will do it as soon as possible_**



Resolving patch:
diff --git a/mindsdb/integrations/handlers/informix_handler/__about__.py b/mindsdb/integrations/handlers/informix_handler/__about__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/informix_handler/__about__.py
@@ -0,0 +1,9 @@
+__title__ = 'MindsDB IBM Informix handler'
+__package_name__ = 'mindsdb_informix_handler'
+__version__ = '0.0.1'
+__description__ = "MindsDB handler for IBM Informix"
+__author__ = 'Parthiv Makwana'
+__github__ = 'https://github.com/mindsdb/mindsdb'
+__pypi__ = 'https://pypi.org/project/mindsdb/'
+__license__ = 'GPL-3.0'
+__copyright__ = 'Copyright 2022- mindsdb'
diff --git a/mindsdb/integrations/handlers/informix_handler/__init__.py b/mindsdb/integrations/handlers/informix_handler/__init__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/informix_handler/__init__.py
@@ -0,0 +1,20 @@
+from mindsdb.integrations.libs.const import HANDLER_TYPE
+
+try:
+    from .informix_handler import InformixHandler as Handler
+    import_error = None
+except Exception as e:
+    Handler = None
+    import_error = e
+from .__about__ import __version__ as version, __description__ as description
+
+
+title = 'IBM Informix'
+name = 'informix'
+type = HANDLER_TYPE.DATA
+icon_path = 'icon.png'
+
+__all__ = [
+    'Handler', 'version', 'name', 'type', 'title', 'description',
+    'import_error', 'icon_path'
+]
diff --git a/mindsdb/integrations/handlers/informix_handler/informix_handler.py b/mindsdb/integrations/handlers/informix_handler/informix_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/informix_handler/informix_handler.py
@@ -0,0 +1,318 @@
+from collections import OrderedDict
+from typing import Optional
+from mindsdb_sql.parser.ast.base import ASTNode
+from mindsdb.integrations.libs.base_handler import DatabaseHandler
+from mindsdb.utilities.log import log
+from mindsdb_sql import parse_sql
+from mindsdb_sql.render.sqlalchemy_render import SqlalchemyRender
+from mindsdb.integrations.libs.response import (
+    HandlerStatusResponse as StatusResponse,
+    HandlerResponse as Response,
+    RESPONSE_TYPE
+)
+from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
+
+
+
+import pandas as pd
+import IfxPyDbi as I 
+from sqlalchemy_informix.ibmdb import  InformixDialect
+
+
+
+class InformixHandler(DatabaseHandler):
+
+
+    name= 'informix'
+
+    def __init__(self, name: str, connection_data: Optional[dict], **kwargs):
+        ''' Initialize the handler
+        Args:
+            name (str): name of particular handler instance
+            connection_data (dict): parameters for connecting to the database
+            **kwargs: arbitrary keyword arguments.
+        '''
+        super().__init__(name)
+        
+        self.kwargs = kwargs
+        self.parser = parse_sql
+        self.loging_enabled = connection_data['loging_enabled'] if 'loging_enabled' in connection_data else True
+        self.server = connection_data['server']
+        self.database = connection_data['database']
+        self.user = connection_data['user']
+        self.password = connection_data['password']
+        self.schemaName = connection_data['schema_name'] 
+        self.host = connection_data['host']
+        self.port = connection_data['port']
+        self.connString = (
+    'SERVER={0};'
+    'DATABASE={1};'
+    'HOST={2};'
+    'PORT={3};'
+    'UID={4};'
+    'PWD={5};').format(self.server, self.database, self.host, self.port, self.user, self.password)
+
+        
+        
+
+        self.connection = None
+        self.is_connected = False
+          
+    
+    def connect(self):
+        ''' Set up any connections required by the handler
+        Should return output of check_connection() method after attempting
+        connection. Should switch self.is_connected.
+        Returns:
+            Connection Object
+        '''
+        if self.is_connected is True:
+            return self.connection
+
+        try:
+            self.connection = I.connect(self.connString,'','')
+  
+            self.is_connected= True
+        except Exception as e:
+            log.error(f'Error while connecting to {self.database}, {e}')
+
+
+        return self.connection
+
+
+    def disconnect(self):
+        ''' Close any existing connections
+        Should switch self.is_connected.
+        '''
+        if self.is_connected is False:
+            return
+        try:
+            self.connection.close()
+            self.is_connected = False
+        except Exception as e:
+            log.error(f'Error while disconnecting to {self.database}, {e}')
+
+        return 
+
+
+    def check_connection(self) -> StatusResponse:
+        ''' Check connection to the handler
+        Returns:
+            HandlerStatusResponse
+        '''
+        responseCode = StatusResponse(False)
+        need_to_close = self.is_connected is False
+
+        try:
+            self.connect()
+            responseCode.success = True
+        except Exception as e:
+            log.error(f'Error connecting to database {self.database}, {e}!')
+            responseCode.error_message = str(e)
+        finally:
+            if responseCode.success is True and need_to_close:
+                self.disconnect()
+            if responseCode.success is False and self.is_connected is True:
+                self.is_connected = False
+
+        return responseCode
+
+
+    def native_query(self, query: str) -> StatusResponse:
+        '''Receive raw query and act upon it somehow.
+        Args:
+            query (Any): query in native format (str for sql databases,
+                dict for mongo, etc)
+        Returns:
+            HandlerResponse
+        '''
+        need_to_close = self.is_connected is False
+        conn = self.connect()
+        cur = conn.cursor()
+        try:
+            cur.execute(query)
+                   
+            if cur._result_set_produced :
+                result = cur.fetchall() 
+                response = Response(
+                    RESPONSE_TYPE.TABLE,
+                    data_frame=pd.DataFrame(
+                        result,
+                        columns=[x[0] for x in cur.description]
+                    )
+                )
+            else:
+                response = Response(RESPONSE_TYPE.OK)
+                if self.loging_enabled:
+                    self.connection.commit()
+        except Exception as e:
+            log.error(f'Error running query: {query} on {self.database}')
+            response = Response(
+                RESPONSE_TYPE.ERROR,
+                error_message=str(e)
+            )
+            if self.loging_enabled:
+                self.connection.rollback()
+
+            cur.close()
+
+
+        if need_to_close is True:
+            self.disconnect()
+
+        return response
+
+    
+    def query(self, query: ASTNode) -> StatusResponse:
+        '''Receive query as AST (abstract syntax tree) and act upon it somehow.
+        Args:
+            query (ASTNode): sql query represented as AST. May be any kind
+                of query: SELECT, INTSERT, DELETE, etc
+        Returns:
+            HandlerResponse
+        '''
+        
+
+
+        renderer = SqlalchemyRender(InformixDialect)
+        query_str = renderer.get_string(query, with_failback=True)
+        return self.native_query(query_str)
+
+
+    def get_tables(self) -> StatusResponse:
+        ''' Return list of entities
+        Return list of entities that will be accesible as tables.
+        Returns:
+            HandlerResponse: shoud have same columns as information_schema.tables
+                (https://dev.mysql.com/doc/refman/8.0/en/information-schema-tables-table.html)
+                Column 'TABLE_NAME' is mandatory, other is optional.
+        '''
+        self.connect()
+
+
+        result=self.connection.tables()
+        try:
+            if result:
+                response = Response(
+                    RESPONSE_TYPE.TABLE,
+                    data_frame=pd.DataFrame(
+                        [x['TABLE_NAME'] for x in result if x['TABLE_SCHEM']==self.schemaName],
+                        columns=['TABLE_NAME']
+                        
+                    )
+                )
+            else:
+                response = Response(RESPONSE_TYPE.OK)
+            
+        except Exception as e:
+            log.error(f'Error running while getting table {e} on ')
+            response = Response(
+                RESPONSE_TYPE.ERROR,
+                error_message=str(e)
+            )
+
+
+
+        return response
+
+    
+    def get_columns(self, table_name: str) -> StatusResponse:
+        ''' Returns a list of entity columns
+        Args:
+            table_name (str): name of one of tables returned by self.get_tables()
+        Returns:
+            HandlerResponse: shoud have same columns as information_schema.columns
+                (https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html)
+                Column 'COLUMN_NAME' is mandatory, other is optional. Hightly
+                recomended to define also 'DATA_TYPE': it should be one of
+                python data types (by default it str).
+        '''
+        
+        self.connect()
+
+
+        result=self.connection.columns(table_name=table_name)
+        try:
+            if result:
+                response = Response(
+                    RESPONSE_TYPE.TABLE,
+                    data_frame=pd.DataFrame(
+                        [result[i]['COLUMN_NAME'] for i in range(len(result)) ],
+                        columns=['COLUMN_NAME']
+                        
+                    )
+                )
+            else:
+                response = Response(RESPONSE_TYPE.OK)
+            
+        except Exception as e:
+            log.error(f'Error running while getting table {e} on ')
+            response = Response(
+                RESPONSE_TYPE.ERROR,
+                error_message=str(e)
+            )
+
+
+
+        return response
+
+        
+
+
+
+
+
+connection_args = OrderedDict(
+    erver={
+        'type': ARG_TYPE.STR,
+        'description': '''
+            The server name you want to get connected.
+        '''
+    },
+    database={
+        'type': ARG_TYPE.STR,
+        'description': '''
+            The database name to use when connecting with the DB2 server.
+        '''
+    },
+    user={
+        'type': ARG_TYPE.STR,
+        'description': 'The user name used to authenticate with the DB2 server.'
+    },
+    password={
+        'type': ARG_TYPE.STR,
+        'description': 'The password to authenticate the user with the DB2 server.'
+    },
+    host={
+        'type': ARG_TYPE.STR,
+        'description': 'The host name or IP address of the DB2 server/database.'
+    },
+    port={
+        'type': ARG_TYPE.INT,
+        'description': 'Specify port to connect DB2 through TCP/IP'
+    },
+    schema_name={
+        'type': ARG_TYPE.STR,
+        'description': 'Specify the schema name for showing tables '
+    },
+    loging_enabled={
+        'type': ARG_TYPE.BOOL,
+        'description': '''
+        Used for COMMIT and ROLLBACK as this command works only for logging enabled database.
+        Note: Its optional.
+        Default is TRUE
+        '''
+    }
+
+)
+
+connection_args_example = OrderedDict(
+    server='server',
+    database='stores_demo',
+    user='informix',
+    password='in4mix',
+    host='127.0.0.1',
+    port='9091',
+    schema_name='Love'
+    
+)


Test patch:
diff --git a/mindsdb/integrations/handlers/informix_handler/tests/__init__.py b/mindsdb/integrations/handlers/informix_handler/tests/__init__.py
new file mode 100644
diff --git a/mindsdb/integrations/handlers/informix_handler/tests/test_informix_handler.py b/mindsdb/integrations/handlers/informix_handler/tests/test_informix_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/informix_handler/tests/test_informix_handler.py
@@ -0,0 +1,54 @@
+import unittest
+from mindsdb.integrations.handlers.informix_handler.informix_handler import InformixHandler
+from mindsdb.api.mysql.mysql_proxy.libs.constants.response_type import RESPONSE_TYPE
+
+
+class InformixHandlerTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.kwargs = {
+            "server": "server",
+            "host": "127.0.0.1",
+            "port": 9093,
+            "user": "informix",
+            "password": "in4mix",
+            "database": "demo",
+            "schema_name": "love",
+            "loging_enabled": False
+        }
+        cls.handler = InformixHandler('test_informix_handler', cls.kwargs)
+
+    def test_0_connect(self):
+         self.handler.connect()
+
+    
+
+    def test_1_drop_table(self):
+        res = self.handler.query("DROP TABLE IF EXISTS LOVE;")
+        assert res.type is  not RESPONSE_TYPE.ERROR 
+
+
+    def test_2_create_table(self):
+        res = self.handler.query("CREATE TABLE IF NOT EXISTS LOVE (LOVER varchar(20));")
+        assert res.type is not  RESPONSE_TYPE.ERROR 
+    
+    def test_3_insert(self):
+        res = self.handler.query("INSERT INTO LOVE VALUES('Hari');")
+        assert res.type is not  RESPONSE_TYPE.ERROR 
+    
+    def test_4_get_tables(self):
+        tables = self.handler.get_tables()
+        assert tables.type is   RESPONSE_TYPE.TABLE
+
+ 
+    def test_5_select_query(self):
+        query = "SELECT * FROM LOVE;"
+        result = self.handler.native_query(query)
+        assert result.type is   RESPONSE_TYPE.TABLE
+
+    def test_5_check_connection(self):
+         self.handler.check_connection()
+
+        
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file