Repository: mindsdb/mindsdb

Issue:
[New Integration]:  👀👀 Oracle Database Integration 👍👍
### Is there an existing integration?

- [X] I have searched the existing integrations.

### Use Case

## Oracle is the most used  Database in the Industry
As it is the most popular SQL database it should be in the integration of MindsDB.

- It would help developers to implement In database AI.
- It would make a headache for developers to implement ML.

### Motivation

As a developer, I think for my developers **`Friends`** and their problems to implement AI.
 As most of the industry is using oracle as it is  `easy + popular`  database .

And it would be great to get a model with an `account + few clicks on the buttons` and ready to predict and use AI Tables in Database.


### Implementation

_No response_

### Anything else?

_No response_


Discussion:
I'd like to work on this issue, could you assign it to me pls?
Sure @eskarimov to start you can check https://docs.mindsdb.com/contribute/integrations/ and review the code of the handlers https://github.com/mindsdb/mindsdb/tree/staging/mindsdb/integrations/handlers. If you have more questions we can discuss this on slack

Resolving patch:
diff --git a/mindsdb/integrations/handlers/oracle_handler/__about__.py b/mindsdb/integrations/handlers/oracle_handler/__about__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/oracle_handler/__about__.py
@@ -0,0 +1,9 @@
+__title__ = "MindsDB Oracle Database handler"
+__package_name__ = "mindsdb_oracle_handler"
+__version__ = "0.0.1"
+__description__ = "MindsDB handler for Oracle Database"
+__author__ = "MindsDB Inc"
+__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/oracle_handler/__init__.py b/mindsdb/integrations/handlers/oracle_handler/__init__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/oracle_handler/__init__.py
@@ -0,0 +1,32 @@
+from mindsdb.integrations.libs.const import HANDLER_TYPE
+
+try:
+    from .oracle_handler import (
+        OracleHandler as Handler,
+        connection_args_example,
+        connection_args,
+    )
+
+    import_error = None
+except Exception as e:
+    Handler = None
+    import_error = e
+from .__about__ import __version__ as version, __description__ as description
+
+title = "Oracle DB"
+name = "oracle"
+type = HANDLER_TYPE.DATA
+icon_path = "icon.svg"
+
+__all__ = [
+    "Handler",
+    "version",
+    "name",
+    "type",
+    "title",
+    "description",
+    "connection_args",
+    "connection_args_example",
+    "import_error",
+    "icon_path",
+]
diff --git a/mindsdb/integrations/handlers/oracle_handler/oracle_handler.py b/mindsdb/integrations/handlers/oracle_handler/oracle_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/oracle_handler/oracle_handler.py
@@ -0,0 +1,196 @@
+from typing import Optional
+from collections import OrderedDict
+
+import pandas as pd
+import oracledb
+from oracledb import connect, Connection, makedsn
+
+from mindsdb_sql.render.sqlalchemy_render import SqlalchemyRender
+from mindsdb_sql.parser.ast.base import ASTNode
+
+from mindsdb.utilities.log import log
+from mindsdb.integrations.libs.base_handler import DatabaseHandler
+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
+
+oracledb.defaults.fetch_lobs = False  # return LOBs directly as strings or bytes
+
+
+class OracleHandler(DatabaseHandler):
+    """
+    This handler handles connection and execution of the Microsoft SQL Server statements.
+    """
+
+    name = "oracle"
+
+    def __init__(self, name: str, connection_data: Optional[dict], **kwargs):
+        super().__init__(name)
+        self.host = connection_data.get("host")
+        self.port = int(connection_data.get("port") or 1521)
+        self.sid = connection_data.get("sid")
+        self.service_name = connection_data.get("service_name")
+        self.user = connection_data.get("user")
+        self.password = connection_data.get("password")
+
+        if self.sid is None and self.service_name is None:
+            raise ValueError("Either 'sid' or 'service_name' must be given")
+        if self.sid and self.service_name:
+            raise ValueError("Only one of 'sid' or 'service_name' must be given")
+
+        if self.sid:
+            self.dsn = makedsn(host=self.host, port=self.port, sid=self.sid)
+        else:
+            self.dsn = makedsn(host=self.host, port=self.port, service_name=self.service_name)
+
+        self.connection = None
+        self.is_connected = False
+
+    def connect(self) -> Connection:
+        if self.is_connected is True:
+            return self.connection
+
+        connection = connect(user=self.user, password=self.password, dsn=self.dsn)
+
+        self.is_connected = True
+        self.connection = connection
+        return self.connection
+
+    def disconnect(self):
+        if self.is_connected:
+            self.connection.close()
+        self.is_connected = False
+        return
+
+    def check_connection(self) -> StatusResponse:
+        """
+        Check the connection of the database
+        :return: success status and error message if error occurs
+        """
+
+        response = StatusResponse(False)
+        need_to_close = self.is_connected is False
+
+        try:
+            con = self.connect()
+            con.ping()
+            response.success = True
+        except Exception as e:
+            log.error(f"Error connecting to Oracle DB {self.dsn}, {e}!")
+            response.error_message = str(e)
+        finally:
+            if response.success is True and need_to_close:
+                self.disconnect()
+            if response.success is False and self.is_connected is True:
+                self.is_connected = False
+        return response
+
+    def native_query(self, query: str) -> Response:
+        """
+        Receive SQL query and runs it
+        :param query: The SQL query to run
+        :return: returns the records from the current recordset
+        """
+        need_to_close = self.is_connected is False
+
+        connection = self.connect()
+        with connection.cursor() as cur:
+            try:
+                cur.execute(query)
+                result = cur.fetchall()
+                if result:
+                    response = Response(
+                        RESPONSE_TYPE.TABLE,
+                        data_frame=pd.DataFrame(
+                            result,
+                            columns=[row[0] for row in cur.description],
+                        ),
+                    )
+                else:
+                    response = Response(RESPONSE_TYPE.OK)
+
+                connection.commit()
+            except Exception as e:
+                log.error(f"Error running query: {query} on {self.dsn}!")
+                response = Response(
+                    RESPONSE_TYPE.ERROR,
+                    error_message=str(e),
+                )
+                connection.rollback()
+
+        if need_to_close is True:
+            self.disconnect()
+
+        return response
+
+    def query(self, query: ASTNode) -> Response:
+        """
+        Retrieve the data from the SQL statement.
+        """
+        renderer = SqlalchemyRender("oracle")
+        query_str = renderer.get_string(query, with_failback=True)
+        return self.native_query(query_str)
+
+    def get_tables(self) -> Response:
+        """
+        List all tables in Oracle DB owned by the current user.
+        """
+        query = """
+            SELECT table_name
+            FROM user_tables
+            ORDER BY 1;
+        """
+        return self.native_query(query)
+
+    def get_columns(self, table_name: str) -> Response:
+        """
+        Show details about the table.
+        """
+        query = f"""
+            SELECT 
+                column_name,
+                data_type
+            FROM USER_TAB_COLUMNS
+            WHERE table_name = '{table_name}'
+        """
+        result = self.native_query(query)
+        return result
+
+
+connection_args = OrderedDict(
+    host={
+        "type": ARG_TYPE.STR,
+        "description": "The host name or IP address of the Oracle DB.",
+    },
+    port={
+        "type": ARG_TYPE.INT,
+        "description": "The TCP/IP port of the Oracle DB. Must be an integer. Default 1521.",
+    },
+    sid={
+        "type": ARG_TYPE.STR,
+        "description": "The site identifier of the Oracle DB. Either sid or service_name should be provided.",
+    },
+    service_name={
+        "type": ARG_TYPE.STR,
+        "description": "The name of the Oracle DB service. Either sid or service_name should be provided.",
+    },
+    user={
+        "type": ARG_TYPE.STR,
+        "description": "The user name used to authenticate against the Oracle DB.",
+    },
+    password={
+        "type": ARG_TYPE.STR,
+        "description": "The password to authenticate the user against Oracle DB.",
+    },
+)
+
+connection_args_example = OrderedDict(
+    host="127.0.0.1",
+    port=1521,
+    user="admin",
+    password="password",
+    sid="ORCL",
+)


Test patch:
diff --git a/mindsdb/integrations/handlers/oracle_handler/tests/__init__.py b/mindsdb/integrations/handlers/oracle_handler/tests/__init__.py
new file mode 100644
diff --git a/mindsdb/integrations/handlers/oracle_handler/tests/test_oracle_handler.py b/mindsdb/integrations/handlers/oracle_handler/tests/test_oracle_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/oracle_handler/tests/test_oracle_handler.py
@@ -0,0 +1,32 @@
+import unittest
+from mindsdb.integrations.handlers.oracle_handler.oracle_handler import OracleHandler
+from mindsdb.api.mysql.mysql_proxy.libs.constants.response_type import RESPONSE_TYPE
+
+
+class OracleHandlerTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.kwargs = {
+            "host": "127.0.0.1",
+            "port": "1521",
+            "user": "admin",
+            "password": "password",
+            "sid": "ORCL"
+        }
+        cls.handler = OracleHandler('test_oracle_handler', **cls.kwargs)
+
+    def test_0_check_connection(self):
+        assert self.handler.check_connection()
+
+    def test_1_native_query_select(self):
+        query = "SELECT * FROM DUAL"
+        result = self.handler.native_query(query)
+        assert result.type is RESPONSE_TYPE.TABLE
+
+    def test_2_get_tables(self):
+        tables = self.handler.get_tables()
+        assert tables.type is not RESPONSE_TYPE.ERROR
+
+    def test_4_get_columns(self):
+        columns = self.handler.get_columns('customers')
+        assert columns.type is not RESPONSE_TYPE.ERROR