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.
@@ -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