Skip to content

Commit 08b4ec6

Browse files
laodouyaauxten
authored andcommitted
Add db api 2.0 driver
1 parent af5c2ae commit 08b4ec6

File tree

10 files changed

+1039
-7
lines changed

10 files changed

+1039
-7
lines changed

chdb/dbapi/__init__.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from .converters import escape_dict, escape_sequence, escape_string
2+
from .constants import FIELD_TYPE
3+
from .err import (
4+
Warning, Error, InterfaceError, DataError,
5+
DatabaseError, OperationalError, IntegrityError, InternalError,
6+
NotSupportedError, ProgrammingError)
7+
from . import connections as _orig_conn
8+
9+
VERSION = (0, 1, 0, None)
10+
if VERSION[3] is not None:
11+
VERSION_STRING = "%d.%d.%d_%s" % VERSION
12+
else:
13+
VERSION_STRING = "%d.%d.%d" % VERSION[:3]
14+
15+
threadsafety = 1
16+
apilevel = "2.0"
17+
paramstyle = "format"
18+
19+
20+
class DBAPISet(frozenset):
21+
22+
def __ne__(self, other):
23+
if isinstance(other, set):
24+
return frozenset.__ne__(self, other)
25+
else:
26+
return other not in self
27+
28+
def __eq__(self, other):
29+
if isinstance(other, frozenset):
30+
return frozenset.__eq__(self, other)
31+
else:
32+
return other in self
33+
34+
def __hash__(self):
35+
return frozenset.__hash__(self)
36+
37+
38+
# TODO it's in pep249 find out meaning and usage of this
39+
# https://www.python.org/dev/peps/pep-0249/#string
40+
STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
41+
FIELD_TYPE.VAR_STRING])
42+
BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
43+
FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
44+
NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
45+
FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
46+
FIELD_TYPE.TINY, FIELD_TYPE.YEAR])
47+
DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE])
48+
TIME = DBAPISet([FIELD_TYPE.TIME])
49+
TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
50+
DATETIME = TIMESTAMP
51+
ROWID = DBAPISet()
52+
53+
54+
def Binary(x):
55+
"""Return x as a binary type."""
56+
return bytes(x)
57+
58+
59+
def Connect(*args, **kwargs):
60+
"""
61+
Connect to the database; see connections.Connection.__init__() for
62+
more information.
63+
"""
64+
from .connections import Connection
65+
return Connection(*args, **kwargs)
66+
67+
68+
if _orig_conn.Connection.__init__.__doc__ is not None:
69+
Connect.__doc__ = _orig_conn.Connection.__init__.__doc__
70+
del _orig_conn
71+
72+
73+
def get_client_info(): # for MySQLdb compatibility
74+
version = VERSION
75+
if VERSION[3] is None:
76+
version = VERSION[:3]
77+
return '.'.join(map(str, version))
78+
79+
80+
connect = Connection = Connect
81+
82+
NULL = "NULL"
83+
84+
__version__ = get_client_info()

chdb/dbapi/connections.py

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import json
2+
from . import err
3+
from .cursors import Cursor
4+
from . import converters
5+
6+
DEBUG = False
7+
VERBOSE = False
8+
9+
10+
class Connection(object):
11+
"""
12+
Representation of a connection with chdb.
13+
14+
The proper way to get an instance of this class is to call
15+
connect().
16+
17+
Accepts several arguments:
18+
19+
:param cursorclass: Custom cursor class to use.
20+
21+
See `Connection <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ in the
22+
specification.
23+
"""
24+
25+
_closed = False
26+
27+
def __init__(self, cursorclass=Cursor):
28+
29+
self._resp = None
30+
31+
# 1. pre-process params in init
32+
self.encoding = 'utf8'
33+
34+
self.cursorclass = cursorclass
35+
36+
self._result = None
37+
self._affected_rows = 0
38+
39+
self.connect()
40+
41+
def connect(self):
42+
self._closed = False
43+
self._execute_command("select 1;")
44+
self._read_query_result()
45+
46+
def close(self):
47+
"""
48+
Send the quit message and close the socket.
49+
50+
See `Connection.close() <https://www.python.org/dev/peps/pep-0249/#Connection.close>`_
51+
in the specification.
52+
53+
:raise Error: If the connection is already closed.
54+
"""
55+
if self._closed:
56+
raise err.Error("Already closed")
57+
self._closed = True
58+
59+
@property
60+
def open(self):
61+
"""Return True if the connection is open"""
62+
return not self._closed
63+
64+
def commit(self):
65+
"""
66+
Commit changes to stable storage.
67+
68+
See `Connection.commit() <https://www.python.org/dev/peps/pep-0249/#commit>`_
69+
in the specification.
70+
"""
71+
return
72+
73+
def rollback(self):
74+
"""
75+
Roll back the current transaction.
76+
77+
See `Connection.rollback() <https://www.python.org/dev/peps/pep-0249/#rollback>`_
78+
in the specification.
79+
"""
80+
return
81+
82+
def cursor(self, cursor=None):
83+
"""
84+
Create a new cursor to execute queries with.
85+
86+
:param cursor: The type of cursor to create; current only :py:class:`Cursor`
87+
None means use Cursor.
88+
"""
89+
if cursor:
90+
return cursor(self)
91+
return self.cursorclass(self)
92+
93+
# The following methods are INTERNAL USE ONLY (called from Cursor)
94+
def query(self, sql):
95+
if isinstance(sql, str):
96+
sql = sql.encode(self.encoding, 'surrogateescape')
97+
self._execute_command(sql)
98+
self._affected_rows = self._read_query_result()
99+
return self._affected_rows
100+
101+
def _execute_command(self, sql):
102+
"""
103+
:raise InterfaceError: If the connection is closed.
104+
:raise ValueError: If no username was specified.
105+
"""
106+
if self._closed:
107+
raise err.InterfaceError("Connection closed")
108+
109+
if isinstance(sql, str):
110+
sql = sql.encode(self.encoding)
111+
112+
if isinstance(sql, bytearray):
113+
sql = bytes(sql)
114+
115+
# drop last command return
116+
if self._resp is not None:
117+
self._resp = None
118+
119+
if DEBUG:
120+
print("DEBUG: query:", sql)
121+
try:
122+
import chdb
123+
self._resp = chdb.query(sql, output_format="JSON").data()
124+
except Exception as error:
125+
raise err.InterfaceError("query err: %s" % error)
126+
127+
def escape(self, obj, mapping=None):
128+
"""Escape whatever value you pass to it.
129+
130+
Non-standard, for internal use; do not use this in your applications.
131+
"""
132+
if isinstance(obj, str):
133+
return "'" + self.escape_string(obj) + "'"
134+
if isinstance(obj, (bytes, bytearray)):
135+
ret = self._quote_bytes(obj)
136+
return ret
137+
return converters.escape_item(obj, mapping=mapping)
138+
139+
def escape_string(self, s):
140+
return converters.escape_string(s)
141+
142+
def _quote_bytes(self, s):
143+
return converters.escape_bytes(s)
144+
145+
def _read_query_result(self):
146+
self._result = None
147+
result = CHDBResult(self)
148+
result.read()
149+
self._result = result
150+
return result.affected_rows
151+
152+
def __enter__(self):
153+
"""Context manager that returns a Cursor"""
154+
return self.cursor()
155+
156+
def __exit__(self, exc, value, traceback):
157+
"""On successful exit, commit. On exception, rollback"""
158+
if exc:
159+
self.rollback()
160+
else:
161+
self.commit()
162+
163+
@property
164+
def resp(self):
165+
return self._resp
166+
167+
168+
class CHDBResult(object):
169+
def __init__(self, connection):
170+
"""
171+
:type connection: Connection
172+
"""
173+
self.connection = connection
174+
self.affected_rows = 0
175+
self.insert_id = None
176+
self.warning_count = 0
177+
self.message = None
178+
self.field_count = 0
179+
self.description = None
180+
self.rows = None
181+
self.has_next = None
182+
183+
def read(self):
184+
try:
185+
data = json.loads(self.connection.resp)
186+
except Exception as error:
187+
raise err.InterfaceError("Unsupported response format:" % error)
188+
189+
try:
190+
self.field_count = len(data["meta"])
191+
description = []
192+
for meta in data["meta"]:
193+
fields = [meta["name"], meta["type"]]
194+
description.append(tuple(fields))
195+
self.description = tuple(description)
196+
197+
rows = []
198+
for line in data["data"]:
199+
row = []
200+
for i in range(self.field_count):
201+
column_data = converters.convert_column_data(self.description[i][1], line[self.description[i][0]])
202+
row.append(column_data)
203+
rows.append(tuple(row))
204+
self.rows = tuple(rows)
205+
except Exception as error:
206+
raise err.InterfaceError("Read return data err:" % error)

chdb/dbapi/constants/FIELD_TYPE.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
DECIMAL = 0
2+
TINY = 1
3+
SHORT = 2
4+
LONG = 3
5+
FLOAT = 4
6+
DOUBLE = 5
7+
NULL = 6
8+
TIMESTAMP = 7
9+
LONGLONG = 8
10+
INT24 = 9
11+
DATE = 10
12+
TIME = 11
13+
DATETIME = 12
14+
YEAR = 13
15+
NEWDATE = 14
16+
VARCHAR = 15
17+
BIT = 16
18+
JSON = 245
19+
NEWDECIMAL = 246
20+
ENUM = 247
21+
SET = 248
22+
TINY_BLOB = 249
23+
MEDIUM_BLOB = 250
24+
LONG_BLOB = 251
25+
BLOB = 252
26+
VAR_STRING = 253
27+
STRING = 254
28+
GEOMETRY = 255
29+
30+
CHAR = TINY
31+
INTERVAL = ENUM
32+

chdb/dbapi/constants/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)