doris-python 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doris_python/__init__.py +25 -0
- doris_python/sqlalchemy/__init__.py +43 -0
- doris_python/sqlalchemy/aiomysql.py +47 -0
- doris_python/sqlalchemy/asyncmy.py +47 -0
- doris_python/sqlalchemy/datatype.py +200 -0
- doris_python/sqlalchemy/dialect.py +393 -0
- doris_python/sqlalchemy/pymysql.py +43 -0
- doris_python-1.0.0.dist-info/METADATA +437 -0
- doris_python-1.0.0.dist-info/RECORD +13 -0
- doris_python-1.0.0.dist-info/WHEEL +5 -0
- doris_python-1.0.0.dist-info/entry_points.txt +6 -0
- doris_python-1.0.0.dist-info/licenses/LICENSE +202 -0
- doris_python-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
|
|
20
|
+
"""Doris dialect base class.
|
|
21
|
+
|
|
22
|
+
Defines the Doris-specific type compiler, DDL compiler, and reflection logic
|
|
23
|
+
that is shared across all driver-specific dialects (mysqldb, pymysql, aiomysql,
|
|
24
|
+
asyncmy). Driver-specific subclasses live in their own modules.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Dict, List
|
|
29
|
+
|
|
30
|
+
from doris_python.sqlalchemy import datatype
|
|
31
|
+
from sqlalchemy import exc, log
|
|
32
|
+
from sqlalchemy import schema as sa_schema
|
|
33
|
+
from sqlalchemy import sql, text
|
|
34
|
+
from sqlalchemy.dialects.mysql.base import (
|
|
35
|
+
MySQLDDLCompiler,
|
|
36
|
+
MySQLDialect,
|
|
37
|
+
MySQLTypeCompiler,
|
|
38
|
+
)
|
|
39
|
+
from sqlalchemy.engine import Connection
|
|
40
|
+
from sqlalchemy.sql.sqltypes import Unicode
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DorisTypeCompiler(MySQLTypeCompiler):
|
|
46
|
+
def _extend_numeric(self, type_, spec):
|
|
47
|
+
# Doris doesn't support UNSIGNED or ZEROFILL
|
|
48
|
+
return spec
|
|
49
|
+
|
|
50
|
+
def visit_TINYINT(self, type_, **kw):
|
|
51
|
+
return "TINYINT"
|
|
52
|
+
|
|
53
|
+
def visit_LARGEINT(self, type_, **kw):
|
|
54
|
+
return "LARGEINT"
|
|
55
|
+
|
|
56
|
+
def visit_DOUBLE(self, type_, **kw):
|
|
57
|
+
return "DOUBLE"
|
|
58
|
+
|
|
59
|
+
def visit_HLL(self, type_, **kw):
|
|
60
|
+
return "HLL"
|
|
61
|
+
|
|
62
|
+
def visit_BITMAP(self, type_, **kw):
|
|
63
|
+
return "BITMAP"
|
|
64
|
+
|
|
65
|
+
def visit_QUANTILE_STATE(self, type_, **kw):
|
|
66
|
+
return "QUANTILE_STATE"
|
|
67
|
+
|
|
68
|
+
def visit_AGG_STATE(self, type_, **kw):
|
|
69
|
+
return "AGG_STATE"
|
|
70
|
+
|
|
71
|
+
def visit_ARRAY(self, type_, **kw):
|
|
72
|
+
return "ARRAY"
|
|
73
|
+
|
|
74
|
+
def visit_MAP(self, type_, **kw):
|
|
75
|
+
return "MAP"
|
|
76
|
+
|
|
77
|
+
def visit_STRUCT(self, type_, **kw):
|
|
78
|
+
return "STRUCT"
|
|
79
|
+
|
|
80
|
+
def visit_IPV4(self, type_, **kw):
|
|
81
|
+
return "IPV4"
|
|
82
|
+
|
|
83
|
+
def visit_IPV6(self, type_, **kw):
|
|
84
|
+
return "IPV6"
|
|
85
|
+
|
|
86
|
+
def visit_TIME(self, type_, **kw):
|
|
87
|
+
return "TIME"
|
|
88
|
+
|
|
89
|
+
def visit_VARIANT(self, type_, **kw):
|
|
90
|
+
return "VARIANT"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DorisDDLCompiler(MySQLDDLCompiler):
|
|
94
|
+
def get_column_specification(self, column, **kw):
|
|
95
|
+
# Override to suppress AUTO_INCREMENT — Doris uses different semantics
|
|
96
|
+
spec = super().get_column_specification(column, **kw)
|
|
97
|
+
spec = spec.replace(" AUTO_INCREMENT", "")
|
|
98
|
+
return spec
|
|
99
|
+
|
|
100
|
+
def visit_primary_key_constraint(self, constraint, **kw):
|
|
101
|
+
# Doris uses KEY model (DUPLICATE/UNIQUE/AGGREGATE) instead of PRIMARY KEY
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
def visit_foreign_key_constraint(self, constraint, **kw):
|
|
105
|
+
# Doris doesn't support foreign keys
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
def visit_unique_constraint(self, constraint, **kw):
|
|
109
|
+
# Doris doesn't support UNIQUE constraints in DDL
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
def post_create_table(self, table):
|
|
113
|
+
opts = table.dialect_options.get("pydoris", {})
|
|
114
|
+
parts = []
|
|
115
|
+
|
|
116
|
+
# ENGINE
|
|
117
|
+
engine = opts.get("engine")
|
|
118
|
+
if engine:
|
|
119
|
+
parts.append("ENGINE = %s" % engine)
|
|
120
|
+
|
|
121
|
+
# KEY model — auto-detect from primary key columns if not specified
|
|
122
|
+
key_type = opts.get("key_type")
|
|
123
|
+
key_columns = opts.get("key_columns")
|
|
124
|
+
if not key_columns:
|
|
125
|
+
pk_cols = [c.name for c in table.primary_key.columns]
|
|
126
|
+
if pk_cols:
|
|
127
|
+
key_columns = pk_cols
|
|
128
|
+
if not key_type:
|
|
129
|
+
key_type = "DUPLICATE"
|
|
130
|
+
if key_type and key_columns:
|
|
131
|
+
preparer = self.preparer
|
|
132
|
+
cols = ", ".join(preparer.quote_identifier(c) for c in key_columns)
|
|
133
|
+
parts.append("%s KEY(%s)" % (key_type, cols))
|
|
134
|
+
|
|
135
|
+
# COMMENT
|
|
136
|
+
if table.comment:
|
|
137
|
+
comment = table.comment.replace("'", "\\'")
|
|
138
|
+
parts.append("COMMENT '%s'" % comment)
|
|
139
|
+
|
|
140
|
+
# PARTITION BY
|
|
141
|
+
partition_by = opts.get("partition_by")
|
|
142
|
+
if partition_by:
|
|
143
|
+
parts.append(partition_by)
|
|
144
|
+
|
|
145
|
+
# DISTRIBUTED BY
|
|
146
|
+
distributed_by = opts.get("distributed_by")
|
|
147
|
+
if distributed_by:
|
|
148
|
+
buckets = opts.get("buckets")
|
|
149
|
+
dist = "DISTRIBUTED BY %s" % distributed_by
|
|
150
|
+
if buckets:
|
|
151
|
+
dist += " BUCKETS %s" % buckets
|
|
152
|
+
parts.append(dist)
|
|
153
|
+
|
|
154
|
+
# PROPERTIES
|
|
155
|
+
properties = opts.get("properties")
|
|
156
|
+
if properties and isinstance(properties, dict):
|
|
157
|
+
props = ", ".join('"%s" = "%s"' % (k, v) for k, v in properties.items())
|
|
158
|
+
parts.append("PROPERTIES (%s)" % props)
|
|
159
|
+
|
|
160
|
+
if parts:
|
|
161
|
+
return "\n" + "\n".join(parts)
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class DorisDialect_base(MySQLDialect):
|
|
166
|
+
"""Base Doris dialect with all Doris-specific behaviour.
|
|
167
|
+
|
|
168
|
+
Driver-specific dialects (``DorisDialect_pymysql``, ``DorisDialect_aiomysql``,
|
|
169
|
+
``DorisDialect_asyncmy``, ``DorisDialect_mysqldb``) inherit from this class
|
|
170
|
+
and the matching ``MySQLDialect_*`` driver class via cooperative
|
|
171
|
+
multiple-inheritance.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
name = "pydoris"
|
|
175
|
+
supports_statement_cache = True
|
|
176
|
+
|
|
177
|
+
type_compiler = DorisTypeCompiler
|
|
178
|
+
ddl_compiler = DorisDDLCompiler
|
|
179
|
+
|
|
180
|
+
construct_arguments = [
|
|
181
|
+
(
|
|
182
|
+
sa_schema.Table,
|
|
183
|
+
{
|
|
184
|
+
"engine": None,
|
|
185
|
+
"key_type": None,
|
|
186
|
+
"key_columns": None,
|
|
187
|
+
"distributed_by": None,
|
|
188
|
+
"buckets": None,
|
|
189
|
+
"partition_by": None,
|
|
190
|
+
"properties": None,
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
def has_table(self, connection, table_name, schema=None, **kw):
|
|
196
|
+
self._ensure_has_table_connection(connection)
|
|
197
|
+
|
|
198
|
+
if schema is None:
|
|
199
|
+
schema = self.default_schema_name
|
|
200
|
+
|
|
201
|
+
rs = connection.execute(
|
|
202
|
+
text(
|
|
203
|
+
"SELECT COUNT(*) FROM information_schema.tables WHERE "
|
|
204
|
+
"table_schema = :table_schema AND "
|
|
205
|
+
"table_name = :table_name"
|
|
206
|
+
).bindparams(
|
|
207
|
+
sql.bindparam("table_schema", type_=Unicode),
|
|
208
|
+
sql.bindparam("table_name", type_=Unicode),
|
|
209
|
+
),
|
|
210
|
+
{
|
|
211
|
+
"table_schema": str(schema),
|
|
212
|
+
"table_name": str(table_name),
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
return bool(rs.scalar())
|
|
216
|
+
|
|
217
|
+
def get_schema_names(self, connection, **kw):
|
|
218
|
+
rp = connection.exec_driver_sql("SHOW schemas")
|
|
219
|
+
return [r[0] for r in rp]
|
|
220
|
+
|
|
221
|
+
def get_table_names(self, connection, schema=None, **kw):
|
|
222
|
+
"""Return a Unicode SHOW TABLES from a given schema."""
|
|
223
|
+
if schema is not None:
|
|
224
|
+
current_schema = schema
|
|
225
|
+
else:
|
|
226
|
+
current_schema = self.default_schema_name
|
|
227
|
+
|
|
228
|
+
charset = self._connection_charset
|
|
229
|
+
|
|
230
|
+
rp = connection.exec_driver_sql(
|
|
231
|
+
"SHOW FULL TABLES FROM %s" % self.identifier_preparer.quote_identifier(current_schema)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return [
|
|
235
|
+
row[0] for row in self._compat_fetchall(rp, charset=charset) if row[1] == "BASE TABLE"
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
def get_view_names(self, connection, schema=None, **kw):
|
|
239
|
+
if schema is None:
|
|
240
|
+
schema = self.default_schema_name
|
|
241
|
+
charset = self._connection_charset
|
|
242
|
+
rp = connection.exec_driver_sql(
|
|
243
|
+
"SHOW FULL TABLES FROM %s" % self.identifier_preparer.quote_identifier(schema)
|
|
244
|
+
)
|
|
245
|
+
return [
|
|
246
|
+
row[0]
|
|
247
|
+
for row in self._compat_fetchall(rp, charset=charset)
|
|
248
|
+
if row[1] in ("VIEW", "SYSTEM VIEW")
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
def get_columns(
|
|
252
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
253
|
+
) -> List[Dict[str, Any]]:
|
|
254
|
+
if not self.has_table(connection, table_name, schema):
|
|
255
|
+
raise exc.NoSuchTableError(f"schema={schema}, table={table_name}")
|
|
256
|
+
schema = schema or self._get_default_schema_name(connection)
|
|
257
|
+
|
|
258
|
+
quote = self.identifier_preparer.quote_identifier
|
|
259
|
+
full_name = quote(table_name)
|
|
260
|
+
if schema:
|
|
261
|
+
full_name = "{}.{}".format(quote(schema), full_name)
|
|
262
|
+
|
|
263
|
+
# SHOW COLUMNS returns: Field(0), Type(1), Null(2), Key(3), Default(4), Extra(5)
|
|
264
|
+
res = connection.exec_driver_sql("SHOW COLUMNS FROM %s" % full_name)
|
|
265
|
+
columns = []
|
|
266
|
+
for record in res:
|
|
267
|
+
column = dict(
|
|
268
|
+
name=record[0],
|
|
269
|
+
type=datatype.parse_sqltype(record[1]),
|
|
270
|
+
nullable=record[2] == "YES",
|
|
271
|
+
default=record[4],
|
|
272
|
+
)
|
|
273
|
+
columns.append(column)
|
|
274
|
+
return columns
|
|
275
|
+
|
|
276
|
+
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
|
|
277
|
+
return { # type: ignore # pep-655 not supported
|
|
278
|
+
"name": None,
|
|
279
|
+
"constrained_columns": [],
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def get_unique_constraints(
|
|
283
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
284
|
+
) -> List[Dict[str, Any]]:
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
def get_check_constraints(
|
|
288
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
289
|
+
) -> List[Dict[str, Any]]:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
def get_foreign_keys(
|
|
293
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
294
|
+
) -> List[Dict[str, Any]]:
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
def get_primary_keys(
|
|
298
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
299
|
+
) -> List[str]:
|
|
300
|
+
pk = self.get_pk_constraint(connection, table_name, schema)
|
|
301
|
+
return pk.get("constrained_columns") # type: ignore
|
|
302
|
+
|
|
303
|
+
def get_indexes(self, connection, table_name, schema=None, **kw):
|
|
304
|
+
quote = self.identifier_preparer.quote_identifier
|
|
305
|
+
full_name = quote(table_name)
|
|
306
|
+
if schema:
|
|
307
|
+
full_name = "{}.{}".format(quote(schema), full_name)
|
|
308
|
+
try:
|
|
309
|
+
# SHOW INDEX returns: Table(0), Non_unique(1), Key_name(2),
|
|
310
|
+
# Seq_in_index(3), Column_name(4), ..., Index_type(10), ...
|
|
311
|
+
rs = connection.exec_driver_sql("SHOW INDEX FROM %s" % full_name)
|
|
312
|
+
indexes = {}
|
|
313
|
+
for row in rs:
|
|
314
|
+
index_name = row[2] # Key_name
|
|
315
|
+
column_name = row[4] # Column_name
|
|
316
|
+
# Doris may return empty string for Non_unique
|
|
317
|
+
non_unique = row[1]
|
|
318
|
+
is_unique = non_unique == "0" or non_unique == 0
|
|
319
|
+
index_type = row[10] if len(row) > 10 else None
|
|
320
|
+
if index_name not in indexes:
|
|
321
|
+
indexes[index_name] = {
|
|
322
|
+
"name": index_name,
|
|
323
|
+
"column_names": [],
|
|
324
|
+
"unique": is_unique,
|
|
325
|
+
}
|
|
326
|
+
if index_type:
|
|
327
|
+
indexes[index_name]["type"] = index_type
|
|
328
|
+
indexes[index_name]["column_names"].append(column_name)
|
|
329
|
+
return list(indexes.values())
|
|
330
|
+
except Exception:
|
|
331
|
+
return []
|
|
332
|
+
|
|
333
|
+
def has_sequence(
|
|
334
|
+
self, connection: Connection, sequence_name: str, schema: str = None, **kw
|
|
335
|
+
) -> bool:
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
def get_sequence_names(self, connection: Connection, schema: str = None, **kw) -> List[str]:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
def get_temp_view_names(self, connection: Connection, schema: str = None, **kw) -> List[str]:
|
|
342
|
+
return []
|
|
343
|
+
|
|
344
|
+
def get_temp_table_names(self, connection: Connection, schema: str = None, **kw) -> List[str]:
|
|
345
|
+
return []
|
|
346
|
+
|
|
347
|
+
def get_table_options(self, connection, table_name, schema=None, **kw):
|
|
348
|
+
return {}
|
|
349
|
+
|
|
350
|
+
def get_table_comment(
|
|
351
|
+
self, connection: Connection, table_name: str, schema: str = None, **kw
|
|
352
|
+
) -> Dict[str, Any]:
|
|
353
|
+
if schema is None:
|
|
354
|
+
schema = self.default_schema_name
|
|
355
|
+
rs = connection.execute(
|
|
356
|
+
text(
|
|
357
|
+
"SELECT table_comment FROM information_schema.tables "
|
|
358
|
+
"WHERE table_schema = :schema AND table_name = :table_name"
|
|
359
|
+
),
|
|
360
|
+
{"schema": str(schema), "table_name": str(table_name)},
|
|
361
|
+
)
|
|
362
|
+
row = rs.fetchone()
|
|
363
|
+
return {"text": row[0] if row else None}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# Driver-specific Doris dialects. Each one combines the Doris-specific
|
|
368
|
+
# reflection / DDL logic from ``DorisDialect_base`` with the driver plumbing
|
|
369
|
+
# from the matching ``sqlalchemy.dialects.mysql`` class.
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb # noqa: E402
|
|
373
|
+
from sqlalchemy.dialects.mysql.pymysql import MySQLDialect_pymysql # noqa: E402
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class DorisDialect_mysqldb(DorisDialect_base, MySQLDialect_mysqldb):
|
|
377
|
+
"""Doris dialect using the ``mysqlclient`` / ``MySQLdb`` driver."""
|
|
378
|
+
|
|
379
|
+
driver = "mysqldb"
|
|
380
|
+
supports_statement_cache = True
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class DorisDialect_pymysql(DorisDialect_base, MySQLDialect_pymysql):
|
|
384
|
+
"""Doris dialect using the pure-Python ``pymysql`` driver."""
|
|
385
|
+
|
|
386
|
+
driver = "pymysql"
|
|
387
|
+
supports_statement_cache = True
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# Backwards-compatibility alias — the original ``DorisDialect`` was registered
|
|
391
|
+
# against the mysqldb driver. Re-export it so existing ``doris://`` URLs
|
|
392
|
+
# (which default to the mysqldb driver) keep working.
|
|
393
|
+
DorisDialect = DorisDialect_mysqldb
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
|
|
3
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
# or more contributor license agreements. See the NOTICE file
|
|
5
|
+
# distributed with this work for additional information
|
|
6
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
# to you under the Apache License, Version 2.0 (the
|
|
8
|
+
# "License"); you may not use this file except in compliance
|
|
9
|
+
# with the License. You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing,
|
|
14
|
+
# software distributed under the License is distributed on an
|
|
15
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
# KIND, either express or implied. See the License for the
|
|
17
|
+
# specific language governing permissions and limitations
|
|
18
|
+
# under the License.
|
|
19
|
+
|
|
20
|
+
r"""
|
|
21
|
+
.. dialect:: doris+pymysql
|
|
22
|
+
:name: Doris via PyMySQL
|
|
23
|
+
:dbapi: pymysql
|
|
24
|
+
:connectstring: doris+pymysql://user:password@host:port/dbname[?key=value&key=value...]
|
|
25
|
+
|
|
26
|
+
The pure-Python PyMySQL driver can also be used to talk to Doris. Use it
|
|
27
|
+
with :func:`_sa.create_engine`::
|
|
28
|
+
|
|
29
|
+
from sqlalchemy import create_engine
|
|
30
|
+
|
|
31
|
+
engine = create_engine("doris+pymysql://user:pass@hostname:9030/dbname")
|
|
32
|
+
""" # noqa: E501
|
|
33
|
+
|
|
34
|
+
from sqlalchemy.dialects.mysql.pymysql import MySQLDialect_pymysql
|
|
35
|
+
|
|
36
|
+
from .dialect import DorisDialect_base
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DorisDialect_pymysql(DorisDialect_base, MySQLDialect_pymysql):
|
|
40
|
+
"""Doris dialect using the ``pymysql`` async driver."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
dialect = DorisDialect_pymysql
|