This is an automated email from the ASF dual-hosted git repository.

yongjiezhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new b4560d442b feat(database): Add OceanBase support (#29496)
b4560d442b is described below

commit b4560d442b874c3ee65265c291e4c4afa6885710
Author: yuanoOo <[email protected]>
AuthorDate: Tue Jul 9 17:26:23 2024 +0800

    feat(database): Add OceanBase support (#29496)
---
 docs/docs/configuration/databases.mdx              |  14 ++
 docs/src/resources/data.js                         |   5 +
 docs/static/img/databases/oceanbase.svg            |  67 ++++++++
 pyproject.toml                                     |   1 +
 superset-frontend/src/assets/images/oceanbase.svg  |  67 ++++++++
 superset/db_engine_specs/oceanbase.py              | 183 +++++++++++++++++++++
 tests/unit_tests/db_engine_specs/test_oceanbase.py |  59 +++++++
 7 files changed, 396 insertions(+)

diff --git a/docs/docs/configuration/databases.mdx 
b/docs/docs/configuration/databases.mdx
index 2b23c464dd..5681acc14d 100644
--- a/docs/docs/configuration/databases.mdx
+++ b/docs/docs/configuration/databases.mdx
@@ -64,6 +64,7 @@ are compatible with Superset.
 | [IBM Db2](/docs/configuration/databases#ibm-db2)                        | 
`pip install ibm_db_sa`                                                         
   | `db2+ibm_db://`                                                            
                                                                            |
 | [IBM Netezza Performance 
Server](/docs/configuration/databases#ibm-netezza-performance-server) | `pip 
install nzalchemy`                                                            | 
`netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>`        
                                                                       |
 | [MySQL](/docs/configuration/databases#mysql)                            | 
`pip install mysqlclient`                                                       
   | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>`          
                                                                            |
+| [OceanBase](/docs/configuration/databases#oceanbase)                    | 
`pip install oceanbase_py`                                                      
   | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>`      
                                                                                
|
 | [Oracle](/docs/configuration/databases#oracle)                          | 
`pip install cx_Oracle`                                                         
   | `oracle://`                                                                
                                                                            |
 | [PostgreSQL](/docs/configuration/databases#postgres)                    | 
`pip install psycopg2`                                                          
   | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>`     
                                                                            |
 | [Presto](/docs/configuration/databases#presto)                          | 
`pip install pyhive`                                                            
   | `presto://`                                                                
                                                                            |
@@ -988,6 +989,19 @@ Here's the recommended connection string:
 netezza+nzpy://{username}:{password}@{hostname}:{port}/{database}
 ```
 
+#### OceanBase
+
+The [sqlalchemy-oceanbase](https://pypi.org/project/oceanbase_py/) library is 
the recommended
+way to connect to OceanBase through SQLAlchemy.
+
+
+The connection string for OceanBase looks like this:
+
+```
+oceanbase://<User>:<Password>@<Host>:<Port>/<Database>
+```
+
+
 #### Ocient DB
 
 The recommended connector library for Ocient is 
[sqlalchemy-ocient](https://pypi.org/project/sqlalchemy-ocient).
diff --git a/docs/src/resources/data.js b/docs/src/resources/data.js
index 42cf835a49..766e32c5dd 100644
--- a/docs/src/resources/data.js
+++ b/docs/src/resources/data.js
@@ -122,4 +122,9 @@ export const Databases = [
     href: 'https://doris.apache.org/',
     imgName: 'doris.png',
   },
+  {
+    title: 'OceanBase',
+    href: 'https://www.oceanbase.com/',
+    imgName: 'oceanbase.svg',
+  },
 ];
diff --git a/docs/static/img/databases/oceanbase.svg 
b/docs/static/img/databases/oceanbase.svg
new file mode 100644
index 0000000000..01e48adbcb
--- /dev/null
+++ b/docs/static/img/databases/oceanbase.svg
@@ -0,0 +1,67 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg"; viewBox="0 
0 95.26 13.73">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #0181fd;
+      }
+
+      .cls-2 {
+        fill: #fff;
+      }
+
+      .cls-3 {
+        fill: #ffa005;
+      }
+
+      .cls-4 {
+        fill: #181818;
+      }
+
+      .cls-5 {
+        fill: #07c846;
+      }
+    </style>
+  </defs>
+  <rect class="cls-2" width="95.26" height="13.73"/>
+  <g>
+    <g>
+      <path class="cls-4" 
d="M51,2.66l-4.58,8.49h2.28l.74-1.48h3.05l.24,1.48h2.28l-1.68-8.49h-2.33Zm-.7,5.34l1.44-2.83,.48,2.83h-1.92Z"/>
+      <path class="cls-4" 
d="M31.98,3.71c-.33-.41-.76-.72-1.28-.95-.52-.23-1.11-.34-1.77-.34s-1.3,.11-1.9,.34c-.6,.23-1.13,.55-1.6,.95-.47,.41-.86,.88-1.18,1.42-.32,.54-.53,1.13-.64,1.76-.11,.63-.1,1.22,.04,1.76,.13,.54,.36,1.02,.69,1.42,.33,.41,.75,.72,1.27,.95,.52,.23,1.11,.34,1.78,.34s1.29-.11,1.89-.34c.6-.23,1.13-.55,1.61-.95,.47-.41,.87-.88,1.19-1.42,.32-.54,.53-1.13,.64-1.76,.11-.63,.1-1.22-.04-1.76-.13-.54-.37-1.02-.7-1.42m-1.84,4.12c-.17,.29-.38,.53-.63,.74-.25,.21-.53,.37-.83,.4
 [...]
+      <polygon class="cls-4" points="46.4 9.28 42.07 9.28 42.32 7.81 46.51 
7.81 46.82 5.94 42.64 5.94 42.88 4.52 47.21 4.52 47.53 2.66 41.06 2.66 39.61 
11.15 46.08 11.15 46.4 9.28"/>
+      <polygon class="cls-4" points="61.29 7.75 58.79 2.66 56.65 2.66 55.2 
11.15 57.34 11.15 58.23 6.05 60.72 11.15 62.85 11.15 64.3 2.66 62.17 2.66 61.29 
7.75"/>
+      <path class="cls-4" 
d="M70.83,8.68c.04-.26,.06-.5,.04-.72-.02-.23-.08-.43-.19-.61s-.25-.33-.45-.46c-.19-.12-.44-.22-.73-.28,.34-.2,.61-.43,.8-.72,.2-.28,.33-.64,.41-1.06,.12-.71,.02-1.25-.31-1.62-.33-.38-.87-.56-1.63-.56h-3.47l-1.45,8.49h3.72c.44,0,.84-.05,1.2-.16,.37-.1,.69-.26,.97-.47,.28-.21,.51-.47,.7-.78,.19-.31,.31-.66,.38-1.06m-3.67-4.34h.67c.67,0,.96,.27,.86,.82-.09,.55-.47,.82-1.14,.82h-.67l.28-1.64Zm1.07,4.88c-.26,.14-.65,.21-1.19,.21h-.75l.31-1.8h.75c.54,0,.91,.07,1.12,.
 [...]
+      <path class="cls-4" 
d="M83.57,5.97c-.16-.06-.33-.12-.5-.17-.17-.05-.32-.11-.45-.19-.13-.07-.23-.16-.3-.25-.07-.1-.09-.22-.07-.37,.04-.22,.15-.39,.35-.53,.2-.14,.43-.2,.71-.2,.22,0,.44,.05,.65,.14,.22,.09,.42,.24,.62,.43l1.14-1.72c-.37-.23-.77-.4-1.19-.51-.42-.12-.84-.17-1.25-.17-.44,0-.84,.07-1.22,.2-.38,.13-.71,.32-.99,.57-.29,.24-.52,.54-.72,.88-.19,.34-.33,.72-.4,1.14-.07,.42-.07,.77,.01,1.04,.08,.27,.21,.5,.39,.68,.18,.18,.39,.32,.63,.43,.25,.11,.5,.21,.76,.3,.22,.08,.4,.16,.56
 [...]
+      <path class="cls-4" 
d="M38.16,2.42c-.57,0-1.14,.11-1.7,.33-.56,.22-1.07,.53-1.54,.92-.46,.39-.86,.86-1.19,1.41-.33,.54-.55,1.14-.66,1.8-.11,.65-.09,1.26,.05,1.81,.15,.55,.38,1.03,.71,1.43,.33,.4,.73,.71,1.21,.93,.48,.22,1.01,.33,1.57,.33,.28,0,.56-.03,.85-.08,.29-.06,.61-.14,.96-.26l.23-.08,.45-2.61c-.65,.6-1.31,.9-2,.9-.31,0-.59-.06-.84-.18-.25-.12-.45-.29-.62-.5-.16-.21-.28-.46-.34-.74-.06-.29-.07-.6,0-.93,.06-.33,.17-.64,.33-.92,.16-.29,.36-.53,.59-.74,.23-.21,.49-.37,.79-.49,.2
 [...]
+      <path class="cls-4" 
d="M78.31,9.36c-.35-.07-1.02-.11-2.18,.17l.26,1.65h2.28l-.36-1.81Z"/>
+      <path class="cls-4" 
d="M78.22,8.87l-.37-1.87c-.55,0-1.24,.09-2.1,.31-.09,.02-.19,.05-.29,.08-.47,.13-.91,.22-1.31,.28l1.25-2.46,.27,1.65c.86-.22,1.55-.3,2.1-.31l-.77-3.85h-2.33l-4.58,8.49h2.28l.82-1.62c.69-.05,1.52-.17,2.46-.43,.14-.04,.27-.07,.39-.1,1.16-.27,1.83-.24,2.18-.17"/>
+      <g>
+        <path class="cls-4" 
d="M89.66,6.01c-1.7,.44-2.99,.4-3.55,.35l-.3,1.83c.16,.01,.35,.02,.56,.03,.84,.02,2.06-.06,3.54-.45,1.4-.37,2.23-.34,2.65-.27l.3-1.83c-.71-.07-1.75-.03-3.21,.35"/>
+        <path class="cls-4" 
d="M90.1,2.73c-1.62,.42-2.87,.41-3.47,.36l-.45,2.8c.4,.04,1.15,.07,2.15-.07l.16-.98c.56-.07,1.19-.18,1.87-.36,1.5-.39,2.34-.34,2.73-.26l.3-1.84c-.71-.09-1.77-.05-3.29,.34"/>
+        <path class="cls-4" 
d="M89.25,9.18c-.54,.14-1.03,.23-1.48,.29l.15-.9c-.62,.07-1.16,.09-1.59,.08-.21,0-.39-.01-.55-.03l-.46,2.72c.18,.01,.39,.03,.64,.03,.84,.02,2.06-.06,3.54-.45,1.32-.35,2.13-.35,2.58-.29l.3-1.83c-.71-.06-1.72,0-3.13,.36"/>
+      </g>
+    </g>
+    <g>
+      <path class="cls-5" 
d="M11.91,12.89c.36-.15,.72-.29,1.1-.42,.17-.06,.35-.12,.52-.17,.14-.04,.28-.08,.42-.12,2.12-.59,4.27-.75,6.35-.52v-2.17c-2.3-.23-4.67-.03-7.02,.63-.14,.04-.28,.08-.42,.12-.18,.06-.37,.12-.55,.18-.36,.12-.72,.26-1.07,.4l.67,2.06Z"/>
+      <path class="cls-1" 
d="M20.29,2.46v6.49c-2.39-.23-4.85-.02-7.27,.69-.3,.09-.59,.18-.89,.28-.36,.12-.72,.24-1.07,.34-3.06,.9-6.16,1.22-9.18,1V4.77c2.36,.23,4.79,.02,7.18-.66,.33-.09,.65-.2,.98-.31,.33-.11,.66-.22,.98-.32,3.09-.92,6.22-1.24,9.27-1.03Z"/>
+      <path class="cls-3" 
d="M10.26,.84c-.36,.15-.72,.29-1.1,.42-.17,.06-.35,.12-.52,.17-.14,.04-.28,.08-.42,.12-2.12,.59-4.27,.75-6.35,.52v2.17c2.3,.23,4.67,.03,7.02-.63,.14-.04,.28-.08,.42-.12,.18-.06,.37-.12,.55-.18,.36-.12,.72-.26,1.07-.4l-.67-2.06Z"/>
+    </g>
+  </g>
+</svg>
diff --git a/pyproject.toml b/pyproject.toml
index 26b020f98c..c6e0c16a21 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -173,6 +173,7 @@ vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
 netezza = ["nzalchemy>=11.0.2"]
 starrocks = ["starrocks>=1.0.0"]
 doris = ["pydoris>=1.0.0, <2.0.0"]
+oceanbase = ["oceanbase_py>=0.0.1"]
 development = [
     "docker",
     "flask-testing",
diff --git a/superset-frontend/src/assets/images/oceanbase.svg 
b/superset-frontend/src/assets/images/oceanbase.svg
new file mode 100644
index 0000000000..01e48adbcb
--- /dev/null
+++ b/superset-frontend/src/assets/images/oceanbase.svg
@@ -0,0 +1,67 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg"; viewBox="0 
0 95.26 13.73">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #0181fd;
+      }
+
+      .cls-2 {
+        fill: #fff;
+      }
+
+      .cls-3 {
+        fill: #ffa005;
+      }
+
+      .cls-4 {
+        fill: #181818;
+      }
+
+      .cls-5 {
+        fill: #07c846;
+      }
+    </style>
+  </defs>
+  <rect class="cls-2" width="95.26" height="13.73"/>
+  <g>
+    <g>
+      <path class="cls-4" 
d="M51,2.66l-4.58,8.49h2.28l.74-1.48h3.05l.24,1.48h2.28l-1.68-8.49h-2.33Zm-.7,5.34l1.44-2.83,.48,2.83h-1.92Z"/>
+      <path class="cls-4" 
d="M31.98,3.71c-.33-.41-.76-.72-1.28-.95-.52-.23-1.11-.34-1.77-.34s-1.3,.11-1.9,.34c-.6,.23-1.13,.55-1.6,.95-.47,.41-.86,.88-1.18,1.42-.32,.54-.53,1.13-.64,1.76-.11,.63-.1,1.22,.04,1.76,.13,.54,.36,1.02,.69,1.42,.33,.41,.75,.72,1.27,.95,.52,.23,1.11,.34,1.78,.34s1.29-.11,1.89-.34c.6-.23,1.13-.55,1.61-.95,.47-.41,.87-.88,1.19-1.42,.32-.54,.53-1.13,.64-1.76,.11-.63,.1-1.22-.04-1.76-.13-.54-.37-1.02-.7-1.42m-1.84,4.12c-.17,.29-.38,.53-.63,.74-.25,.21-.53,.37-.83,.4
 [...]
+      <polygon class="cls-4" points="46.4 9.28 42.07 9.28 42.32 7.81 46.51 
7.81 46.82 5.94 42.64 5.94 42.88 4.52 47.21 4.52 47.53 2.66 41.06 2.66 39.61 
11.15 46.08 11.15 46.4 9.28"/>
+      <polygon class="cls-4" points="61.29 7.75 58.79 2.66 56.65 2.66 55.2 
11.15 57.34 11.15 58.23 6.05 60.72 11.15 62.85 11.15 64.3 2.66 62.17 2.66 61.29 
7.75"/>
+      <path class="cls-4" 
d="M70.83,8.68c.04-.26,.06-.5,.04-.72-.02-.23-.08-.43-.19-.61s-.25-.33-.45-.46c-.19-.12-.44-.22-.73-.28,.34-.2,.61-.43,.8-.72,.2-.28,.33-.64,.41-1.06,.12-.71,.02-1.25-.31-1.62-.33-.38-.87-.56-1.63-.56h-3.47l-1.45,8.49h3.72c.44,0,.84-.05,1.2-.16,.37-.1,.69-.26,.97-.47,.28-.21,.51-.47,.7-.78,.19-.31,.31-.66,.38-1.06m-3.67-4.34h.67c.67,0,.96,.27,.86,.82-.09,.55-.47,.82-1.14,.82h-.67l.28-1.64Zm1.07,4.88c-.26,.14-.65,.21-1.19,.21h-.75l.31-1.8h.75c.54,0,.91,.07,1.12,.
 [...]
+      <path class="cls-4" 
d="M83.57,5.97c-.16-.06-.33-.12-.5-.17-.17-.05-.32-.11-.45-.19-.13-.07-.23-.16-.3-.25-.07-.1-.09-.22-.07-.37,.04-.22,.15-.39,.35-.53,.2-.14,.43-.2,.71-.2,.22,0,.44,.05,.65,.14,.22,.09,.42,.24,.62,.43l1.14-1.72c-.37-.23-.77-.4-1.19-.51-.42-.12-.84-.17-1.25-.17-.44,0-.84,.07-1.22,.2-.38,.13-.71,.32-.99,.57-.29,.24-.52,.54-.72,.88-.19,.34-.33,.72-.4,1.14-.07,.42-.07,.77,.01,1.04,.08,.27,.21,.5,.39,.68,.18,.18,.39,.32,.63,.43,.25,.11,.5,.21,.76,.3,.22,.08,.4,.16,.56
 [...]
+      <path class="cls-4" 
d="M38.16,2.42c-.57,0-1.14,.11-1.7,.33-.56,.22-1.07,.53-1.54,.92-.46,.39-.86,.86-1.19,1.41-.33,.54-.55,1.14-.66,1.8-.11,.65-.09,1.26,.05,1.81,.15,.55,.38,1.03,.71,1.43,.33,.4,.73,.71,1.21,.93,.48,.22,1.01,.33,1.57,.33,.28,0,.56-.03,.85-.08,.29-.06,.61-.14,.96-.26l.23-.08,.45-2.61c-.65,.6-1.31,.9-2,.9-.31,0-.59-.06-.84-.18-.25-.12-.45-.29-.62-.5-.16-.21-.28-.46-.34-.74-.06-.29-.07-.6,0-.93,.06-.33,.17-.64,.33-.92,.16-.29,.36-.53,.59-.74,.23-.21,.49-.37,.79-.49,.2
 [...]
+      <path class="cls-4" 
d="M78.31,9.36c-.35-.07-1.02-.11-2.18,.17l.26,1.65h2.28l-.36-1.81Z"/>
+      <path class="cls-4" 
d="M78.22,8.87l-.37-1.87c-.55,0-1.24,.09-2.1,.31-.09,.02-.19,.05-.29,.08-.47,.13-.91,.22-1.31,.28l1.25-2.46,.27,1.65c.86-.22,1.55-.3,2.1-.31l-.77-3.85h-2.33l-4.58,8.49h2.28l.82-1.62c.69-.05,1.52-.17,2.46-.43,.14-.04,.27-.07,.39-.1,1.16-.27,1.83-.24,2.18-.17"/>
+      <g>
+        <path class="cls-4" 
d="M89.66,6.01c-1.7,.44-2.99,.4-3.55,.35l-.3,1.83c.16,.01,.35,.02,.56,.03,.84,.02,2.06-.06,3.54-.45,1.4-.37,2.23-.34,2.65-.27l.3-1.83c-.71-.07-1.75-.03-3.21,.35"/>
+        <path class="cls-4" 
d="M90.1,2.73c-1.62,.42-2.87,.41-3.47,.36l-.45,2.8c.4,.04,1.15,.07,2.15-.07l.16-.98c.56-.07,1.19-.18,1.87-.36,1.5-.39,2.34-.34,2.73-.26l.3-1.84c-.71-.09-1.77-.05-3.29,.34"/>
+        <path class="cls-4" 
d="M89.25,9.18c-.54,.14-1.03,.23-1.48,.29l.15-.9c-.62,.07-1.16,.09-1.59,.08-.21,0-.39-.01-.55-.03l-.46,2.72c.18,.01,.39,.03,.64,.03,.84,.02,2.06-.06,3.54-.45,1.32-.35,2.13-.35,2.58-.29l.3-1.83c-.71-.06-1.72,0-3.13,.36"/>
+      </g>
+    </g>
+    <g>
+      <path class="cls-5" 
d="M11.91,12.89c.36-.15,.72-.29,1.1-.42,.17-.06,.35-.12,.52-.17,.14-.04,.28-.08,.42-.12,2.12-.59,4.27-.75,6.35-.52v-2.17c-2.3-.23-4.67-.03-7.02,.63-.14,.04-.28,.08-.42,.12-.18,.06-.37,.12-.55,.18-.36,.12-.72,.26-1.07,.4l.67,2.06Z"/>
+      <path class="cls-1" 
d="M20.29,2.46v6.49c-2.39-.23-4.85-.02-7.27,.69-.3,.09-.59,.18-.89,.28-.36,.12-.72,.24-1.07,.34-3.06,.9-6.16,1.22-9.18,1V4.77c2.36,.23,4.79,.02,7.18-.66,.33-.09,.65-.2,.98-.31,.33-.11,.66-.22,.98-.32,3.09-.92,6.22-1.24,9.27-1.03Z"/>
+      <path class="cls-3" 
d="M10.26,.84c-.36,.15-.72,.29-1.1,.42-.17,.06-.35,.12-.52,.17-.14,.04-.28,.08-.42,.12-2.12,.59-4.27,.75-6.35,.52v2.17c2.3,.23,4.67,.03,7.02-.63,.14-.04,.28-.08,.42-.12,.18-.06,.37-.12,.55-.18,.36-.12,.72-.26,1.07-.4l-.67-2.06Z"/>
+    </g>
+  </g>
+</svg>
diff --git a/superset/db_engine_specs/oceanbase.py 
b/superset/db_engine_specs/oceanbase.py
new file mode 100644
index 0000000000..3f4e2d4ed9
--- /dev/null
+++ b/superset/db_engine_specs/oceanbase.py
@@ -0,0 +1,183 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import logging
+import re
+from re import Pattern
+from typing import Any, Optional
+
+from flask_babel import gettext as __
+from sqlalchemy import Numeric, TEXT, types
+from sqlalchemy.sql.type_api import TypeEngine
+
+from superset.db_engine_specs.mysql import MySQLEngineSpec
+from superset.errors import SupersetErrorType
+from superset.utils.core import GenericDataType
+
+# Regular expressions to catch custom errors
+CONNECTION_ACCESS_DENIED_REGEX = re.compile(
+    "Access denied for user '(?P<username>.*?)'"
+)
+CONNECTION_INVALID_HOSTNAME_REGEX = re.compile(
+    "Unknown OceanBase server host '(?P<hostname>.*?)'"
+)
+CONNECTION_UNKNOWN_DATABASE_REGEX = re.compile("Unknown database 
'(?P<database>.*?)'")
+CONNECTION_HOST_DOWN_REGEX = re.compile(
+    "Can't connect to OceanBase server on '(?P<hostname>.*?)'"
+)
+SYNTAX_ERROR_REGEX = re.compile(
+    "check the manual that corresponds to your OceanBase server "
+    "version for the right syntax to use near '(?P<server_error>.*)"
+)
+
+logger = logging.getLogger(__name__)
+
+
+class NUMBER(Numeric):
+    __visit_name__ = "NUMBER"
+
+
+class NUMERIC(Numeric):
+    __visit_name__ = "NUMERIC"
+
+
+class ARRAY(TypeEngine):
+    __visit_name__ = "ARRAY"
+
+    @property
+    def python_type(self) -> Optional[type[list[Any]]]:
+        return list
+
+
+class MAP(TypeEngine):
+    __visit_name__ = "MAP"
+
+    @property
+    def python_type(self) -> Optional[type[dict[Any, Any]]]:
+        return dict
+
+
+class OceanBaseEngineSpec(MySQLEngineSpec):
+    engine = "oceanbase"
+    engine_aliases = {"oceanbase", "oceanbase_py"}
+    engine_name = "OceanBase"
+    max_column_name_length = 128
+    default_driver = "oceanbase"
+
+    sqlalchemy_uri_placeholder = (
+        "oceanbase://user:password@host:port/db[?key=value&key=value...]"
+    )
+    encryption_parameters = {"ssl": "0"}
+    supports_dynamic_schema = True
+
+    column_type_mappings = (  # type: ignore
+        (
+            re.compile(r"^tinyint", re.IGNORECASE),
+            types.SMALLINT(),
+            GenericDataType.NUMERIC,
+        ),
+        (
+            re.compile(r"^largeint", re.IGNORECASE),
+            types.BIGINT(),
+            GenericDataType.NUMERIC,
+        ),
+        (
+            re.compile(r"^decimal.*", re.IGNORECASE),
+            types.DECIMAL(),
+            GenericDataType.NUMERIC,
+        ),
+        (
+            re.compile(r"^double", re.IGNORECASE),
+            types.FLOAT(),
+            GenericDataType.NUMERIC,
+        ),
+        (
+            re.compile(r"^varchar(\((\d+)\))*$", re.IGNORECASE),
+            types.VARCHAR(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^char(\((\d+)\))*$", re.IGNORECASE),
+            types.CHAR(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^json.*", re.IGNORECASE),
+            types.JSON(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^binary.*", re.IGNORECASE),
+            types.BINARY(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^array.*", re.IGNORECASE),
+            ARRAY(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^map.*", re.IGNORECASE),
+            MAP(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^text.*", re.IGNORECASE),
+            TEXT(),
+            GenericDataType.STRING,
+        ),
+        (
+            re.compile(r"^number.*", re.IGNORECASE),
+            NUMBER(),
+            GenericDataType.NUMERIC,
+        ),
+        (
+            re.compile(r"^numeric.*", re.IGNORECASE),
+            NUMERIC(),
+            GenericDataType.NUMERIC,
+        ),
+    )
+
+    custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, 
Any]]] = {
+        CONNECTION_ACCESS_DENIED_REGEX: (
+            __('Either the username "%(username)s" or the password is 
incorrect.'),
+            SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
+            {"invalid": ["username", "password"]},
+        ),
+        CONNECTION_INVALID_HOSTNAME_REGEX: (
+            __('Unknown OceanBase server host "%(hostname)s".'),
+            SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
+            {"invalid": ["host"]},
+        ),
+        CONNECTION_HOST_DOWN_REGEX: (
+            __('The host "%(hostname)s" might be down and can\'t be reached.'),
+            SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
+            {"invalid": ["host", "port"]},
+        ),
+        CONNECTION_UNKNOWN_DATABASE_REGEX: (
+            __('Unable to connect to database "%(database)s".'),
+            SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
+            {"invalid": ["database"]},
+        ),
+        SYNTAX_ERROR_REGEX: (
+            __(
+                'Please check your query for syntax errors near 
"%(server_error)s". '
+                "Then, try running your query again."
+            ),
+            SupersetErrorType.SYNTAX_ERROR,
+            {},
+        ),
+    }
diff --git a/tests/unit_tests/db_engine_specs/test_oceanbase.py 
b/tests/unit_tests/db_engine_specs/test_oceanbase.py
new file mode 100644
index 0000000000..87fddb63ec
--- /dev/null
+++ b/tests/unit_tests/db_engine_specs/test_oceanbase.py
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Any, Optional
+
+import pytest
+from sqlalchemy import JSON, types
+
+from superset.db_engine_specs.oceanbase import ARRAY, MAP, NUMBER, NUMERIC
+from superset.utils.core import GenericDataType
+from tests.unit_tests.db_engine_specs.utils import assert_column_spec
+
+
[email protected](
+    "native_type,sqla_type,attrs,generic_type,is_dttm",
+    [
+        # Numeric
+        ("tinyint", types.SMALLINT, None, GenericDataType.NUMERIC, False),
+        ("largeint", types.BIGINT, None, GenericDataType.NUMERIC, False),
+        ("decimal(38,18)", types.DECIMAL, None, GenericDataType.NUMERIC, 
False),
+        ("number(38,18)", NUMBER, None, GenericDataType.NUMERIC, False),
+        ("numeric(38,18)", NUMERIC, None, GenericDataType.NUMERIC, False),
+        ("double", types.FLOAT, None, GenericDataType.NUMERIC, False),
+        # String
+        ("char(10)", types.CHAR, None, GenericDataType.STRING, False),
+        ("varchar(65533)", types.VARCHAR, None, GenericDataType.STRING, False),
+        ("binary", types.BINARY, None, GenericDataType.STRING, False),
+        ("text", types.TEXT, None, GenericDataType.STRING, False),
+        # Complex type
+        ("array<varchar(65533)>", ARRAY, None, GenericDataType.STRING, False),
+        ("map<string,int>", MAP, None, GenericDataType.STRING, False),
+        ("json", JSON, None, GenericDataType.STRING, False),
+        ("jsonb", JSON, None, GenericDataType.STRING, False),
+    ],
+)
+def test_get_column_spec(
+    native_type: str,
+    sqla_type: type[types.TypeEngine],
+    attrs: Optional[dict[str, Any]],
+    generic_type: GenericDataType,
+    is_dttm: bool,
+) -> None:
+    from superset.db_engine_specs.oceanbase import OceanBaseEngineSpec as spec
+
+    assert_column_spec(spec, native_type, sqla_type, attrs, generic_type, 
is_dttm)

Reply via email to