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

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


The following commit(s) were added to refs/heads/master by this push:
     new a7466e551a [IOTDB-3466] Python client add support for SQLAlchemy 
Dialect (#6255)
a7466e551a is described below

commit a7466e551a1e6aef41ab45d1630b611f6ed33097
Author: Leiyang <[email protected]>
AuthorDate: Fri Jun 17 14:40:22 2022 +0800

    [IOTDB-3466] Python client add support for SQLAlchemy Dialect (#6255)
---
 client-py/README.md                                |  95 ++++++++
 client-py/iotdb/dbapi/Cursor.py                    |  10 +-
 client-py/iotdb/sqlalchemy/IoTDBDialect.py         | 136 ++++++++++++
 .../sqlalchemy/IoTDBIdentifierPreparer.py}         |  14 +-
 client-py/iotdb/sqlalchemy/IoTDBSQLCompiler.py     | 243 +++++++++++++++++++++
 .../sqlalchemy/IoTDBTypeCompiler.py}               |  32 ++-
 .../sqlalchemy/__init__.py}                        |   6 -
 .../sqlalchemy/tests/__init__.py}                  |   6 -
 client-py/iotdb/sqlalchemy/tests/test_dialect.py   |  92 ++++++++
 client-py/requirements.txt                         |   5 +-
 client-py/setup.py                                 |   7 +
 .../UserGuide/API/Programming-Python-Native-API.md | 100 ++++++++-
 .../UserGuide/API/Programming-Python-Native-API.md |  97 +++++++-
 13 files changed, 814 insertions(+), 29 deletions(-)

diff --git a/client-py/README.md b/client-py/README.md
index 3166acacc4..beef55e351 100644
--- a/client-py/README.md
+++ b/client-py/README.md
@@ -273,6 +273,11 @@ session.execute_query_statement(sql)
 session.execute_non_query_statement(sql)
 ```
 
+* Execute statement
+
+```python
+session.execute_statement(sql)
+```
 
 ### Schema Template
 #### Create Schema Template
@@ -463,6 +468,96 @@ cursor.close()
 conn.close()
 ```
 
+### IoTDB SQLAlchemy Dialect (Experimental)
+The SQLAlchemy dialect of IoTDB is written to adapt to Apache Superset.
+This part is still being improved.
+Please do not use it in the production environment!
+#### Mapping of the metadata
+The data model used by SQLAlchemy is a relational data model, which describes 
the relationships between different entities through tables.
+While the data model of IoTDB is a hierarchical data model, which organizes 
the data through a tree structure.
+In order to adapt IoTDB to the dialect of SQLAlchemy, the original data model 
in IoTDB needs to be reorganized.
+Converting the data model of IoTDB into the data model of SQLAlchemy.
+
+The metadata in the IoTDB are:
+
+1. Storage Group
+2. Path
+3. Entity
+4. Measurement
+
+The metadata in the SQLAlchemy are:
+1. Schema
+2. Table
+3. Column
+
+The mapping relationship between them is:
+
+| The metadata in the SQLAlchemy | The metadata in the IoTDB                   
         |
+| -------------------- | ---------------------------------------------- |
+| Schema               | Storage Group                                  |
+| Table                | Path ( from storage group to entity ) + Entity |
+| Column               | Measurement                                    |
+
+The following figure shows the relationship between the two more intuitively:
+
+![sqlalchemy-to-iotdb](https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/API/IoTDB-SQLAlchemy/sqlalchemy-to-iotdb.png?raw=true)
+
+#### Data type mapping
+| data type in IoTDB | data type in SQLAlchemy |
+|--------------------|-------------------------|
+| BOOLEAN            | Boolean                 |
+| INT32              | Integer                 |
+| INT64              | BigInteger              |
+| FLOAT              | Float                   |
+| DOUBLE             | Float                   |
+| TEXT               | Text                    |
+| LONG               | BigInteger              |
+#### Example
+
++ execute statement
+
+```python
+from sqlalchemy import create_engine
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+connect = engine.connect()
+result = connect.execute("SELECT ** FROM root")
+for row in result.fetchall():
+    print(row)
+```
+
++ ORM (now only simple queries are supported)
+
+```python
+from sqlalchemy import create_engine, Column, Float, BigInteger, MetaData
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+metadata = MetaData(
+    schema='root.factory'
+)
+Base = declarative_base(metadata=metadata)
+
+
+class Device(Base):
+    __tablename__ = "room2.device1"
+    Time = Column(BigInteger, primary_key=True)
+    temperature = Column(Float)
+    status = Column(Float)
+
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+
+DbSession = sessionmaker(bind=engine)
+session = DbSession()
+
+res = session.query(Device.status).filter(Device.temperature > 1)
+
+for row in res:
+    print(row)
+```
+
+
 ## Developers
 
 ### Introduction
diff --git a/client-py/iotdb/dbapi/Cursor.py b/client-py/iotdb/dbapi/Cursor.py
index fd680835ba..a1d6e2caab 100644
--- a/client-py/iotdb/dbapi/Cursor.py
+++ b/client-py/iotdb/dbapi/Cursor.py
@@ -111,6 +111,7 @@ class Cursor(object):
             sql = operation % parameters
 
         time_index = []
+        time_names = []
         if self.__sqlalchemy_mode:
             sql_seqs = []
             seqs = sql.split("\n")
@@ -120,6 +121,10 @@ class Cursor(object):
                         int(index)
                         for index in seq.replace("FROM Time Index", "").split()
                     ]
+                elif seq.find("FROM Time Name") >= 0:
+                    time_names = [
+                        name for name in seq.replace("FROM Time Name", 
"").split()
+                    ]
                 else:
                     sql_seqs.append(seq)
             sql = "\n".join(sql_seqs)
@@ -137,8 +142,8 @@ class Cursor(object):
                     time_column = data.columns[0]
                     time_column_value = data.Time
                     del data[time_column]
-                    for index in time_index:
-                        data.insert(index, time_column + str(index), 
time_column_value)
+                    for i in range(len(time_index)):
+                        data.insert(time_index[i], time_names[i], 
time_column_value)
 
                 col_names = data.columns.tolist()
                 col_types = data_set.get_column_types()
@@ -152,6 +157,7 @@ class Cursor(object):
                 "row_count": len(rows),
             }
         except Exception:
+            logger.error("failed to execute statement:{}".format(sql))
             self.__result = {
                 "col_names": None,
                 "col_types": None,
diff --git a/client-py/iotdb/sqlalchemy/IoTDBDialect.py 
b/client-py/iotdb/sqlalchemy/IoTDBDialect.py
new file mode 100644
index 0000000000..baf5d6525d
--- /dev/null
+++ b/client-py/iotdb/sqlalchemy/IoTDBDialect.py
@@ -0,0 +1,136 @@
+# 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 sqlalchemy import types, util
+from sqlalchemy.engine import default
+from sqlalchemy.sql.sqltypes import String
+
+from iotdb import dbapi
+
+from .IoTDBSQLCompiler import IoTDBSQLCompiler
+from .IoTDBTypeCompiler import IoTDBTypeCompiler
+from .IoTDBIdentifierPreparer import IoTDBIdentifierPreparer
+
+TYPES_MAP = {
+    "BOOLEAN": types.Boolean,
+    "INT32": types.Integer,
+    "INT64": types.BigInteger,
+    "FLOAT": types.Float,
+    "DOUBLE": types.Float,
+    "TEXT": types.Text,
+    "LONG": types.BigInteger,
+}
+
+
+class IoTDBDialect(default.DefaultDialect):
+    name = "iotdb"
+    driver = "iotdb-python"
+    statement_compiler = IoTDBSQLCompiler
+    type_compiler = IoTDBTypeCompiler
+    preparer = IoTDBIdentifierPreparer
+    convert_unicode = True
+
+    supports_unicode_statements = True
+    supports_unicode_binds = True
+    supports_simple_order_by_label = False
+    supports_schemas = True
+    supports_right_nested_joins = False
+    description_encoding = None
+
+    if hasattr(String, "RETURNS_UNICODE"):
+        returns_unicode_strings = String.RETURNS_UNICODE
+    else:
+
+        def _check_unicode_returns(self, connection, additional_tests=None):
+            return True
+
+        _check_unicode_returns = _check_unicode_returns
+
+    def create_connect_args(self, url):
+        # inherits the docstring from interfaces.Dialect.create_connect_args
+        opts = url.translate_connect_args()
+        opts.update(url.query)
+        opts.update({"sqlalchemy_mode": True})
+        return [[], opts]
+
+    @classmethod
+    def dbapi(cls):
+        return dbapi
+
+    def has_schema(self, connection, schema):
+        return schema in self.get_schema_names(connection)
+
+    def has_table(self, connection, table_name, schema=None, **kw):
+        return table_name in self.get_table_names(connection, schema=schema)
+
+    def get_schema_names(self, connection, **kw):
+        cursor = connection.execute("SHOW STORAGE GROUP")
+        return [row[0] for row in cursor.fetchall()]
+
+    def get_table_names(self, connection, schema=None, **kw):
+        cursor = connection.execute(
+            "SHOW DEVICES %s.**" % (schema or self.default_schema_name)
+        )
+        return [row[0].replace(schema + ".", "", 1) for row in 
cursor.fetchall()]
+
+    def get_columns(self, connection, table_name, schema=None, **kw):
+        cursor = connection.execute("SHOW TIMESERIES %s.%s.*" % (schema, 
table_name))
+        columns = [self._general_time_column_info()]
+        for row in cursor.fetchall():
+            columns.append(self._create_column_info(row, schema, table_name))
+        return columns
+
+    def get_pk_constraint(self, connection, table_name, schema=None, **kw):
+        pass
+
+    def get_foreign_keys(self, connection, table_name, schema=None, **kw):
+        return []
+
+    def get_indexes(self, connection, table_name, schema=None, **kw):
+        return []
+
+    @util.memoized_property
+    def _dialect_specific_select_one(self):
+        # IoTDB does not support select 1
+        # so replace the statement with "show version"
+        return "SHOW VERSION"
+
+    def _general_time_column_info(self):
+        """
+        Treat Time as a column
+        """
+        return {
+            "name": "Time",
+            "type": self._resolve_type("LONG"),
+            "nullable": False,
+            "default": None,
+        }
+
+    def _create_column_info(self, row, schema, table_name):
+        """
+        Generate description information for each column
+        """
+        return {
+            "name": row[0].replace(schema + "." + table_name + ".", "", 1),
+            "type": self._resolve_type(row[3]),
+            "nullable": True,
+            "default": None,
+        }
+
+    def _resolve_type(self, type_):
+        return TYPES_MAP.get(type_, types.UserDefinedType)
diff --git a/client-py/requirements.txt 
b/client-py/iotdb/sqlalchemy/IoTDBIdentifierPreparer.py
similarity index 72%
copy from client-py/requirements.txt
copy to client-py/iotdb/sqlalchemy/IoTDBIdentifierPreparer.py
index 566c75f79c..e09dd3c230 100644
--- a/client-py/requirements.txt
+++ b/client-py/iotdb/sqlalchemy/IoTDBIdentifierPreparer.py
@@ -16,8 +16,12 @@
 # under the License.
 #
 
-# Pandas Export
-pandas~=1.3.5
-# Testcontainer
-testcontainers==3.3.0
-numpy~=1.21.4
\ No newline at end of file
+from sqlalchemy.sql.compiler import IdentifierPreparer
+
+
+class IoTDBIdentifierPreparer(IdentifierPreparer):
+    def __init__(self, dialect, **kw):
+        quote = "`"
+        super(IoTDBIdentifierPreparer, self).__init__(
+            dialect, initial_quote=quote, escape_quote=quote, **kw
+        )
diff --git a/client-py/iotdb/sqlalchemy/IoTDBSQLCompiler.py 
b/client-py/iotdb/sqlalchemy/IoTDBSQLCompiler.py
new file mode 100644
index 0000000000..36c4ca0bf7
--- /dev/null
+++ b/client-py/iotdb/sqlalchemy/IoTDBSQLCompiler.py
@@ -0,0 +1,243 @@
+# 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 sqlalchemy.sql.compiler import SQLCompiler
+from sqlalchemy.sql.compiler import OPERATORS
+from sqlalchemy.sql import operators
+
+
+class IoTDBSQLCompiler(SQLCompiler):
+    def order_by_clause(self, select, **kw):
+        """allow dialects to customize how ORDER BY is rendered."""
+
+        order_by = select._order_by_clause._compiler_dispatch(self, **kw)
+        if "Time" in order_by:
+            return " ORDER BY " + order_by.replace('"', "")
+        else:
+            return ""
+
+    def group_by_clause(self, select, **kw):
+        """allow dialects to customize how GROUP BY is rendered."""
+        return ""
+
+    def visit_select(
+        self,
+        select,
+        asfrom=False,
+        parens=True,
+        fromhints=None,
+        compound_index=0,
+        nested_join_translation=False,
+        select_wraps_for=None,
+        lateral=False,
+        **kwargs,
+    ):
+        """
+        Override this method to solve two problems
+        1. IoTDB does not support querying Time as a measurement name (e.g. 
select Time from root.storagegroup.device)
+        2. IoTDB does not support path.measurement format to determine a 
column (e.g. select root.storagegroup.device.temperature from 
root.storagegroup.device)
+        """
+        needs_nested_translation = (
+            select.use_labels
+            and not nested_join_translation
+            and not self.stack
+            and not self.dialect.supports_right_nested_joins
+        )
+
+        if needs_nested_translation:
+            transformed_select = 
self._transform_select_for_nested_joins(select)
+            text = self.visit_select(
+                transformed_select,
+                asfrom=asfrom,
+                parens=parens,
+                fromhints=fromhints,
+                compound_index=compound_index,
+                nested_join_translation=True,
+                **kwargs,
+            )
+
+        toplevel = not self.stack
+        entry = self._default_stack_entry if toplevel else self.stack[-1]
+
+        populate_result_map = need_column_expressions = (
+            toplevel
+            or entry.get("need_result_map_for_compound", False)
+            or entry.get("need_result_map_for_nested", False)
+        )
+
+        if compound_index > 0:
+            populate_result_map = False
+
+        # this was first proposed as part of #3372; however, it is not
+        # reached in current tests and could possibly be an assertion
+        # instead.
+        if not populate_result_map and "add_to_result_map" in kwargs:
+            del kwargs["add_to_result_map"]
+
+        if needs_nested_translation:
+            if populate_result_map:
+                self._transform_result_map_for_nested_joins(select, 
transformed_select)
+            return text
+
+        froms = self._setup_select_stack(select, entry, asfrom, lateral)
+
+        column_clause_args = kwargs.copy()
+        column_clause_args.update(
+            {"within_label_clause": False, "within_columns_clause": False}
+        )
+
+        text = "SELECT "  # we're off to a good start !
+
+        if select._hints:
+            hint_text, byfrom = self._setup_select_hints(select)
+            if hint_text:
+                text += hint_text + " "
+        else:
+            byfrom = None
+
+        if select._prefixes:
+            text += self._generate_prefixes(select, select._prefixes, **kwargs)
+
+        text += self.get_select_precolumns(select, **kwargs)
+        # the actual list of columns to print in the SELECT column list.
+        # IoTDB does not support querying Time as a measurement name (e.g. 
select Time from root.storagegroup.device)
+        columns = []
+        for name, column in select._columns_plus_names:
+            column.table = None
+            columns.append(
+                self._label_select_column(
+                    select,
+                    column,
+                    populate_result_map,
+                    asfrom,
+                    column_clause_args,
+                    name=name,
+                    need_column_expressions=need_column_expressions,
+                )
+            )
+        inner_columns = [c for c in columns if c is not None]
+
+        if populate_result_map and select_wraps_for is not None:
+            # if this select is a compiler-generated wrapper,
+            # rewrite the targeted columns in the result map
+
+            translate = dict(
+                zip(
+                    [name for (key, name) in select._columns_plus_names],
+                    [name for (key, name) in 
select_wraps_for._columns_plus_names],
+                )
+            )
+
+            self._result_columns = [
+                (key, name, tuple(translate.get(o, o) for o in obj), type_)
+                for key, name, obj, type_ in self._result_columns
+            ]
+        # IoTDB does not allow to query Time as column,
+        # need to filter out Time and pass Time and Time's alias to DBAPI 
separately
+        # to achieve the query of Time by encoding.
+        time_column_index = []
+        time_column_names = []
+        for i in range(len(inner_columns)):
+            column_strs = (
+                inner_columns[i].replace(self.preparer.initial_quote, 
"").split()
+            )
+            if "Time" in column_strs:
+                time_column_index.append(str(i))
+                time_column_names.append(
+                    column_strs[2]
+                    if OPERATORS[operators.as_] in column_strs
+                    else column_strs[0]
+                )
+        # delete Time column
+        inner_columns = list(
+            filter(
+                lambda x: "Time"
+                not in x.replace(self.preparer.initial_quote, "").split(),
+                inner_columns,
+            )
+        )
+        if inner_columns and time_column_index:
+            inner_columns[-1] = (
+                inner_columns[-1]
+                + " \n FROM Time Index "
+                + " ".join(time_column_index)
+                + "\n FROM Time Name "
+                + " ".join(time_column_names)
+            )
+
+        text = self._compose_select_body(
+            text, select, inner_columns, froms, byfrom, kwargs
+        )
+
+        if select._statement_hints:
+            per_dialect = [
+                ht
+                for (dialect_name, ht) in select._statement_hints
+                if dialect_name in ("*", self.dialect.name)
+            ]
+            if per_dialect:
+                text += " " + self.get_statement_hint_text(per_dialect)
+
+        if self.ctes and toplevel:
+            text = self._render_cte_clause() + text
+
+        if select._suffixes:
+            text += " " + self._generate_prefixes(select, select._suffixes, 
**kwargs)
+
+        self.stack.pop(-1)
+
+        if (asfrom or lateral) and parens:
+            return "(" + text + ")"
+        else:
+            return text
+
+    def visit_table(
+        self,
+        table,
+        asfrom=False,
+        iscrud=False,
+        ashint=False,
+        fromhints=None,
+        use_schema=True,
+        **kwargs,
+    ):
+        """
+        IoTDB's table does not support quotation marks (e.g. select ** from 
`root.`)
+        need to override this method
+        """
+        if asfrom or ashint:
+            effective_schema = self.preparer.schema_for_object(table)
+
+            if use_schema and effective_schema:
+                ret = effective_schema + "." + table.name
+            else:
+                ret = table.name
+            if fromhints and table in fromhints:
+                ret = self.format_from_hint_text(ret, table, fromhints[table], 
iscrud)
+            return ret
+        else:
+            return ""
+
+    def visit_column(
+        self, column, add_to_result_map=None, include_table=True, **kwargs
+    ):
+        """
+        IoTDB's where statement does not support "table".column format(e.g. 
"table".column > 1)
+        need to override this method to return the name of column directly
+        """
+        return column.name
diff --git a/client-py/requirements.txt 
b/client-py/iotdb/sqlalchemy/IoTDBTypeCompiler.py
similarity index 55%
copy from client-py/requirements.txt
copy to client-py/iotdb/sqlalchemy/IoTDBTypeCompiler.py
index 566c75f79c..4cfd2480bd 100644
--- a/client-py/requirements.txt
+++ b/client-py/iotdb/sqlalchemy/IoTDBTypeCompiler.py
@@ -16,8 +16,30 @@
 # under the License.
 #
 
-# Pandas Export
-pandas~=1.3.5
-# Testcontainer
-testcontainers==3.3.0
-numpy~=1.21.4
\ No newline at end of file
+from sqlalchemy.sql.compiler import GenericTypeCompiler
+
+
+class IoTDBTypeCompiler(GenericTypeCompiler):
+    def visit_FLOAT(self, type_, **kw):
+        return "FLOAT"
+
+    def visit_NUMERIC(self, type_, **kw):
+        return "INT64"
+
+    def visit_DECIMAL(self, type_, **kw):
+        return "DOUBLE"
+
+    def visit_INTEGER(self, type_, **kw):
+        return "INT32"
+
+    def visit_SMALLINT(self, type_, **kw):
+        return "INT32"
+
+    def visit_BIGINT(self, type_, **kw):
+        return "LONG"
+
+    def visit_TIMESTAMP(self, type_, **kw):
+        return "LONG"
+
+    def visit_text(self, type_, **kw):
+        return "TEXT"
diff --git a/client-py/requirements.txt b/client-py/iotdb/sqlalchemy/__init__.py
similarity index 90%
copy from client-py/requirements.txt
copy to client-py/iotdb/sqlalchemy/__init__.py
index 566c75f79c..2a1e720805 100644
--- a/client-py/requirements.txt
+++ b/client-py/iotdb/sqlalchemy/__init__.py
@@ -15,9 +15,3 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
-# Pandas Export
-pandas~=1.3.5
-# Testcontainer
-testcontainers==3.3.0
-numpy~=1.21.4
\ No newline at end of file
diff --git a/client-py/requirements.txt 
b/client-py/iotdb/sqlalchemy/tests/__init__.py
similarity index 90%
copy from client-py/requirements.txt
copy to client-py/iotdb/sqlalchemy/tests/__init__.py
index 566c75f79c..2a1e720805 100644
--- a/client-py/requirements.txt
+++ b/client-py/iotdb/sqlalchemy/tests/__init__.py
@@ -15,9 +15,3 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
-# Pandas Export
-pandas~=1.3.5
-# Testcontainer
-testcontainers==3.3.0
-numpy~=1.21.4
\ No newline at end of file
diff --git a/client-py/iotdb/sqlalchemy/tests/test_dialect.py 
b/client-py/iotdb/sqlalchemy/tests/test_dialect.py
new file mode 100644
index 0000000000..7b0e3e2ad6
--- /dev/null
+++ b/client-py/iotdb/sqlalchemy/tests/test_dialect.py
@@ -0,0 +1,92 @@
+# 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 operator
+
+from sqlalchemy import create_engine, inspect
+from sqlalchemy.dialects import registry
+
+from iotdb.IoTDBContainer import IoTDBContainer
+
+final_flag = True
+failed_count = 0
+
+
+def test_fail():
+    global failed_count
+    global final_flag
+    final_flag = False
+    failed_count += 1
+
+
+def print_message(message):
+    print("*********")
+    print(message)
+    print("*********")
+
+
+def test_dialect():
+    with IoTDBContainer("iotdb:dev") as db:
+        db: IoTDBContainer
+        url = (
+            "iotdb://root:root@"
+            + db.get_container_host_ip()
+            + ":"
+            + db.get_exposed_port(6667)
+        )
+        registry.register("iotdb", "iotdb.sqlalchemy.IoTDBDialect", 
"IoTDBDialect")
+        eng = create_engine(url)
+        eng.execute("create storage group root.cursor")
+        eng.execute("create storage group root.cursor_s1")
+        eng.execute(
+            "create timeseries root.cursor.device1.temperature with 
datatype=FLOAT,encoding=RLE"
+        )
+        eng.execute(
+            "create timeseries root.cursor.device1.status with 
datatype=FLOAT,encoding=RLE"
+        )
+        eng.execute(
+            "create timeseries root.cursor.device2.temperature with 
datatype=FLOAT,encoding=RLE"
+        )
+        insp = inspect(eng)
+        # test get_schema_names
+        schema_names = insp.get_schema_names()
+        if not operator.eq(schema_names, ["root.cursor", "root.cursor_s1"]):
+            test_fail()
+            print_message("test get_schema_names failed!")
+        # test get_table_names
+        table_names = insp.get_table_names("root.cursor")
+        if not operator.eq(table_names, ["device1", "device2"]):
+            test_fail()
+            print_message("test get_table_names failed!")
+        # test get_columns
+        columns = insp.get_columns(table_name="device1", schema="root.cursor")
+        if len(columns) != 3:
+            test_fail()
+            print_message("test get_columns failed!")
+        eng.execute("delete storage group root.cursor")
+        eng.execute("delete storage group root.cursor_s1")
+        # close engine
+        eng.dispose()
+
+
+if final_flag:
+    print("All executions done!!")
+else:
+    print("Some test failed, please have a check")
+    print("failed count: ", failed_count)
+    exit(1)
diff --git a/client-py/requirements.txt b/client-py/requirements.txt
index 566c75f79c..8715d50595 100644
--- a/client-py/requirements.txt
+++ b/client-py/requirements.txt
@@ -20,4 +20,7 @@
 pandas~=1.3.5
 # Testcontainer
 testcontainers==3.3.0
-numpy~=1.21.4
\ No newline at end of file
+numpy~=1.21.4
+# SQLAlchemy Dialect
+sqlalchemy == 1.3.20
+sqlalchemy-utils == 0.36.8
\ No newline at end of file
diff --git a/client-py/setup.py b/client-py/setup.py
index 11e30cf40f..a3f147af3c 100644
--- a/client-py/setup.py
+++ b/client-py/setup.py
@@ -44,6 +44,8 @@ setuptools.setup(
         "pandas>=1.0.0,<1.99.99",
         "numpy>=1.0.0",
         "testcontainers>=2.0.0",
+        "sqlalchemy>=1.3.16, <1.4, !=1.3.21",
+        "sqlalchemy-utils>=0.37.8, <0.38",
     ],
     classifiers=[
         "Programming Language :: Python :: 3",
@@ -55,4 +57,9 @@ setuptools.setup(
     python_requires=">=3.7",
     license="Apache License, Version 2.0",
     website="https://iotdb.apache.org";,
+    entry_points={
+        "sqlalchemy.dialects": [
+            "iotdb = iotdb.sqlalchemy.IoTDBDialect:IoTDBDialect",
+        ],
+    },
 )
diff --git a/docs/UserGuide/API/Programming-Python-Native-API.md 
b/docs/UserGuide/API/Programming-Python-Native-API.md
index c3e39e6cd7..0f962ebb44 100644
--- a/docs/UserGuide/API/Programming-Python-Native-API.md
+++ b/docs/UserGuide/API/Programming-Python-Native-API.md
@@ -251,6 +251,11 @@ session.execute_query_statement(sql)
 session.execute_non_query_statement(sql)
 ```
 
+* Execute statement
+
+```python
+session.execute_statement(sql)
+```
 
 ### Schema Template
 #### Create Schema Template
@@ -407,7 +412,7 @@ cursor = conn.cursor()
 ```
 + simple SQL statement execution
 ```python
-cursor.execute("SELECT * FROM root.*")
+cursor.execute("SELECT ** FROM root")
 for row in cursor.fetchall():
     print(row)
 ```
@@ -416,7 +421,7 @@ for row in cursor.fetchall():
 
 IoTDB DBAPI supports pyformat style parameters
 ```python
-cursor.execute("SELECT * FROM root.* WHERE time < 
%(time)s",{"time":"2017-11-01T00:08:00.000"})
+cursor.execute("SELECT ** FROM root WHERE time < 
%(time)s",{"time":"2017-11-01T00:08:00.000"})
 for row in cursor.fetchall():
     print(row)
 ```
@@ -440,6 +445,97 @@ cursor.close()
 conn.close()
 ```
 
+### IoTDB SQLAlchemy Dialect (Experimental)
+The SQLAlchemy dialect of IoTDB is written to adapt to Apache Superset.
+This part is still being improved.
+Please do not use it in the production environment!
+#### Mapping of the metadata
+The data model used by SQLAlchemy is a relational data model, which describes 
the relationships between different entities through tables.
+While the data model of IoTDB is a hierarchical data model, which organizes 
the data through a tree structure.
+In order to adapt IoTDB to the dialect of SQLAlchemy, the original data model 
in IoTDB needs to be reorganized.
+Converting the data model of IoTDB into the data model of SQLAlchemy.
+
+The metadata in the IoTDB are:
+
+1. Storage Group
+2. Path
+3. Entity
+4. Measurement
+
+The metadata in the SQLAlchemy are:
+1. Schema
+2. Table
+3. Column
+
+The mapping relationship between them is:
+
+| The metadata in the SQLAlchemy | The metadata in the IoTDB                   
         |
+| -------------------- | ---------------------------------------------- |
+| Schema               | Storage Group                                  |
+| Table                | Path ( from storage group to entity ) + Entity |
+| Column               | Measurement                                    |
+
+The following figure shows the relationship between the two more intuitively:
+
+![sqlalchemy-to-iotdb](https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/API/IoTDB-SQLAlchemy/sqlalchemy-to-iotdb.png?raw=true)
+
+#### Data type mapping
+| data type in IoTDB | data type in SQLAlchemy |
+|--------------------|-------------------------|
+| BOOLEAN            | Boolean                 |
+| INT32              | Integer                 |
+| INT64              | BigInteger              |
+| FLOAT              | Float                   |
+| DOUBLE             | Float                   |
+| TEXT               | Text                    |
+| LONG               | BigInteger              |
+
+#### Example
+
++ execute statement
+
+```python
+from sqlalchemy import create_engine
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+connect = engine.connect()
+result = connect.execute("SELECT ** FROM root")
+for row in result.fetchall():
+    print(row)
+```
+
++ ORM (now only simple queries are supported)
+
+```python
+from sqlalchemy import create_engine, Column, Float, BigInteger, MetaData
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+metadata = MetaData(
+    schema='root.factory'
+)
+Base = declarative_base(metadata=metadata)
+
+
+class Device(Base):
+    __tablename__ = "room2.device1"
+    Time = Column(BigInteger, primary_key=True)
+    temperature = Column(Float)
+    status = Column(Float)
+
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+
+DbSession = sessionmaker(bind=engine)
+session = DbSession()
+
+res = session.query(Device.status).filter(Device.temperature > 1)
+
+for row in res:
+    print(row)
+```
+
+
 ## Developers
 
 ### Introduction
diff --git a/docs/zh/UserGuide/API/Programming-Python-Native-API.md 
b/docs/zh/UserGuide/API/Programming-Python-Native-API.md
index 645e281008..9f385081bd 100644
--- a/docs/zh/UserGuide/API/Programming-Python-Native-API.md
+++ b/docs/zh/UserGuide/API/Programming-Python-Native-API.md
@@ -246,6 +246,12 @@ session.execute_query_statement(sql)
 session.execute_non_query_statement(sql)
 ```
 
+* 执行语句
+
+```python
+session.execute_statement(sql)
+```
+
 
 ### 元数据模版接口
 #### 构建元数据模版
@@ -401,7 +407,7 @@ cursor = conn.cursor()
 ```
 + 执行简单的SQL语句
 ```python
-cursor.execute("SELECT * FROM root.*")
+cursor.execute("SELECT ** FROM root")
 for row in cursor.fetchall():
     print(row)
 ```
@@ -410,7 +416,7 @@ for row in cursor.fetchall():
 
 IoTDB DBAPI 支持pyformat风格的参数
 ```python
-cursor.execute("SELECT * FROM root.* WHERE time < 
%(time)s",{"time":"2017-11-01T00:08:00.000"})
+cursor.execute("SELECT ** FROM root WHERE time < 
%(time)s",{"time":"2017-11-01T00:08:00.000"})
 for row in cursor.fetchall():
     print(row)
 ```
@@ -434,6 +440,93 @@ cursor.close()
 conn.close()
 ```
 
+### IoTDB SQLAlchemy Dialect(实验性)
+IoTDB的SQLAlchemy方言主要是为了适配Apache superset而编写的,该部分仍在完善中,请勿在生产环境中使用!
+#### 元数据模型映射
+SQLAlchemy 所使用的数据模型为关系数据模型,这种数据模型通过表格来描述不同实体之间的关系。
+而 IoTDB 的数据模型为层次数据模型,通过树状结构来对数据进行组织。
+为了使 IoTDB 能够适配 SQLAlchemy 的方言,需要对 IoTDB 中原有的数据模型进行重新组织,
+把 IoTDB 的数据模型转换成 SQLAlchemy 的数据模型。
+
+IoTDB 中的元数据有:
+
+1. Storage Group:存储组
+2. Path:存储路径
+3. Entity:实体
+4. Measurement:物理量
+
+SQLAlchemy 中的元数据有:
+1. Schema:数据模式
+2. Table:数据表
+3. Column:数据列
+
+它们之间的映射关系为:
+
+| SQLAlchemy中的元数据 | IoTDB中对应的元数据                            |
+| -------------------- | ---------------------------------------------- |
+| Schema               | Storage Group                                  |
+| Table                | Path ( from storage group to entity ) + Entity |
+| Column               | Measurement                                    |
+
+下图更加清晰的展示了二者的映射关系:
+
+![sqlalchemy-to-iotdb](https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/API/IoTDB-SQLAlchemy/sqlalchemy-to-iotdb.png?raw=true)
+
+#### 数据类型映射
+| IoTDB 中的数据类型 | SQLAlchemy 中的数据类型 |
+|--------------|-------------------|
+| BOOLEAN      | Boolean           |
+| INT32        | Integer           |
+| INT64        | BigInteger        |
+| FLOAT        | Float             |
+| DOUBLE       | Float             |
+| TEXT         | Text              |
+| LONG         | BigInteger        |
+#### Example
+
++ 执行语句
+
+```python
+from sqlalchemy import create_engine
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+connect = engine.connect()
+result = connect.execute("SELECT ** FROM root")
+for row in result.fetchall():
+    print(row)
+```
+
++ ORM (目前只支持简单的查询)
+
+```python
+from sqlalchemy import create_engine, Column, Float, BigInteger, MetaData
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+metadata = MetaData(
+    schema='root.factory'
+)
+Base = declarative_base(metadata=metadata)
+
+
+class Device(Base):
+    __tablename__ = "room2.device1"
+    Time = Column(BigInteger, primary_key=True)
+    temperature = Column(Float)
+    status = Column(Float)
+
+
+engine = create_engine("iotdb://root:[email protected]:6667")
+
+DbSession = sessionmaker(bind=engine)
+session = DbSession()
+
+res = session.query(Device.status).filter(Device.temperature > 1)
+
+for row in res:
+    print(row)
+```
+
 ## 给开发人员
 
 ### 介绍

Reply via email to