Skip to content

Commit 18d4aa4

Browse files
authored
Merge pull request #283 from chdb-io/interactiveState
SQLite like API mode of chDB
2 parents 8a84df2 + 7c3cdde commit 18d4aa4

File tree

22 files changed

+1299
-350
lines changed

22 files changed

+1299
-350
lines changed

.clang-tidy

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Checks: [
2727
'-bugprone-not-null-terminated-result',
2828
'-bugprone-reserved-identifier', # useful but too slow, TODO retry when https://reviews.llvm.org/rG1c282052624f9d0bd273bde0b47b30c96699c6c7 is merged
2929
'-bugprone-unchecked-optional-access',
30+
'-bugprone-crtp-constructor-accessibility',
31+
'-bugprone-suspicious-stringview-data-usage',
3032

3133
'-cert-dcl16-c',
3234
'-cert-dcl37-c',
@@ -36,6 +38,7 @@ Checks: [
3638
'-cert-msc51-cpp',
3739
'-cert-oop54-cpp',
3840
'-cert-oop57-cpp',
41+
'-cert-err33-c', # Misreports on clang-19: it warns about all functions containing 'remove' in the name, not only about the standard library.
3942

4043
'-clang-analyzer-optin.performance.Padding',
4144

@@ -94,10 +97,12 @@ Checks: [
9497
'-modernize-pass-by-value',
9598
'-modernize-return-braced-init-list',
9699
'-modernize-use-auto',
100+
'-modernize-use-constraints', # This is a good check, but clang-tidy crashes, see https://github.com/llvm/llvm-project/issues/91872
97101
'-modernize-use-default-member-init',
98102
'-modernize-use-emplace',
99103
'-modernize-use-nodiscard',
100104
'-modernize-use-trailing-return-type',
105+
'-modernize-use-designated-initializers',
101106

102107
'-performance-enum-size',
103108
'-performance-inefficient-string-concatenation',
@@ -121,7 +126,8 @@ Checks: [
121126
'-readability-magic-numbers',
122127
'-readability-named-parameter',
123128
'-readability-redundant-declaration',
124-
'-readability-redundant-inline-specifier',
129+
'-readability-redundant-inline-specifier', # useful but incompatible with __attribute((always_inline))__ (aka. ALWAYS_INLINE, base/base/defines.h).
130+
# ALWAYS_INLINE only has an effect if combined with `inline`: https://godbolt.org/z/Eefd74qdM
125131
'-readability-redundant-member-init', # Useful but triggers another problem. Imagine a struct S with multiple String members. Structs are often instantiated via designated
126132
# initializer S s{.s1 = [...], .s2 = [...], [...]}. In this case, compiler warning `missing-field-initializers` requires to specify all members which are not in-struct
127133
# initialized (example: s1 in struct S { String s1; String s2{};}; is not in-struct initialized, therefore it must be specified at instantiation time). As explicitly
@@ -132,12 +138,7 @@ Checks: [
132138
'-readability-uppercase-literal-suffix',
133139
'-readability-use-anyofallof',
134140

135-
'-zircon-*',
136-
137-
# This is a good check, but clang-tidy crashes, see https://github.com/llvm/llvm-project/issues/91872
138-
'-modernize-use-constraints',
139-
# https://github.com/abseil/abseil-cpp/issues/1667
140-
'-clang-analyzer-optin.core.EnumCastOutOfRange'
141+
'-zircon-*'
141142
]
142143

143144
WarningsAsErrors: '*'
@@ -172,4 +173,4 @@ CheckOptions:
172173
performance-move-const-arg.CheckTriviallyCopyableMove: false
173174
# Workaround clang-tidy bug: https://github.com/llvm/llvm-project/issues/46097
174175
readability-identifier-naming.TypeTemplateParameterIgnoredRegexp: expr-type
175-
cppcoreguidelines-avoid-do-while.IgnoreMacros: true
176+
cppcoreguidelines-avoid-do-while.IgnoreMacros: true

.cursorignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
contrib/

chdb/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ def query(sql, output_format="CSV", path="", udf_path=""):
8585
PyReader = _chdb.PyReader
8686

8787
from . import dbapi, session, udf, utils # noqa: E402
88+
from .state import connect # noqa: E402
8889

8990
__all__ = [
91+
"_chdb",
9092
"PyReader",
9193
"ChdbError",
9294
"query",
@@ -99,4 +101,5 @@ def query(sql, output_format="CSV", path="", udf_path=""):
99101
"session",
100102
"udf",
101103
"utils",
104+
"connect",
102105
]

chdb/dbapi/connections.py

Lines changed: 36 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import json
21
from . import err
32
from .cursors import Cursor
43
from . import converters
4+
from ..state import sqlitelike as chdb_stateful
55

66
DEBUG = False
77
VERBOSE = False
@@ -10,152 +10,78 @@
1010
class Connection(object):
1111
"""
1212
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-
:param path: Optional folder path to store database files on disk.
21-
22-
See `Connection <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ in the
23-
specification.
2413
"""
2514

26-
_closed = False
27-
_session = None
28-
29-
def __init__(self, cursorclass=Cursor, path=None):
30-
31-
self._resp = None
32-
33-
# 1. pre-process params in init
34-
self.encoding = 'utf8'
35-
36-
self.cursorclass = cursorclass
37-
38-
self._result = None
15+
def __init__(self, path=None):
16+
self._closed = False
17+
self.encoding = "utf8"
3918
self._affected_rows = 0
19+
self._resp = None
4020

41-
self.connect(path)
21+
# Initialize sqlitelike connection
22+
connection_string = ":memory:" if path is None else f"file:{path}"
23+
self._conn = chdb_stateful.Connection(connection_string)
4224

43-
def connect(self, path=None):
44-
from chdb import session as chs
45-
self._session = chs.Session(path)
46-
self._closed = False
47-
self._execute_command("select 1;")
48-
self._read_query_result()
25+
# Test connection with a simple query
26+
cursor = self._conn.cursor()
27+
cursor.execute("SELECT 1")
28+
cursor.close()
4929

5030
def close(self):
51-
"""
52-
Send the quit message and close the socket.
53-
54-
See `Connection.close() <https://www.python.org/dev/peps/pep-0249/#Connection.close>`_
55-
in the specification.
56-
57-
:raise Error: If the connection is already closed.
58-
"""
31+
"""Send the quit message and close the socket."""
5932
if self._closed:
6033
raise err.Error("Already closed")
6134
self._closed = True
62-
self._session = None
35+
self._conn.close()
6336

6437
@property
6538
def open(self):
6639
"""Return True if the connection is open"""
6740
return not self._closed
6841

6942
def commit(self):
70-
"""
71-
Commit changes to stable storage.
72-
73-
See `Connection.commit() <https://www.python.org/dev/peps/pep-0249/#commit>`_
74-
in the specification.
75-
"""
76-
return
43+
"""Commit changes to stable storage."""
44+
# No-op for ClickHouse
45+
pass
7746

7847
def rollback(self):
79-
"""
80-
Roll back the current transaction.
81-
82-
See `Connection.rollback() <https://www.python.org/dev/peps/pep-0249/#rollback>`_
83-
in the specification.
84-
"""
85-
return
48+
"""Roll back the current transaction."""
49+
# No-op for ClickHouse
50+
pass
8651

8752
def cursor(self, cursor=None):
88-
"""
89-
Create a new cursor to execute queries with.
90-
91-
:param cursor: The type of cursor to create; current only :py:class:`Cursor`
92-
None means use Cursor.
93-
"""
53+
"""Create a new cursor to execute queries with."""
54+
if self._closed:
55+
raise err.Error("Connection closed")
9456
if cursor:
95-
return cursor(self)
96-
return self.cursorclass(self)
57+
return Cursor(self)
58+
return Cursor(self)
9759

98-
# The following methods are INTERNAL USE ONLY (called from Cursor)
99-
def query(self, sql):
100-
if isinstance(sql, str):
101-
sql = sql.encode(self.encoding, 'surrogateescape')
102-
self._execute_command(sql)
103-
self._affected_rows = self._read_query_result()
104-
return self._affected_rows
105-
106-
def _execute_command(self, sql):
107-
"""
108-
:raise InterfaceError: If the connection is closed.
109-
:raise ValueError: If no username was specified.
110-
"""
60+
def query(self, sql, fmt="ArrowStream"):
61+
"""Execute a query and return the raw result."""
11162
if self._closed:
11263
raise err.InterfaceError("Connection closed")
11364

11465
if isinstance(sql, str):
115-
sql = sql.encode(self.encoding)
66+
sql = sql.encode(self.encoding, "surrogateescape")
11667

117-
if isinstance(sql, bytearray):
118-
sql = bytes(sql)
119-
120-
# drop last command return
121-
if self._resp is not None:
122-
self._resp = None
123-
124-
if DEBUG:
125-
print("DEBUG: query:", sql)
12668
try:
127-
res = self._session.query(sql, fmt="JSON")
128-
if res.has_error():
129-
raise err.DatabaseError(res.error_message())
130-
self._resp = res.data()
69+
result = self._conn.query(sql.decode(), fmt)
70+
self._resp = result
71+
return result
13172
except Exception as error:
132-
raise err.InterfaceError("query err: %s" % error)
73+
raise err.InterfaceError(f"Query error: {error}")
13374

13475
def escape(self, obj, mapping=None):
135-
"""Escape whatever value you pass to it.
136-
137-
Non-standard, for internal use; do not use this in your applications.
138-
"""
139-
if isinstance(obj, str):
140-
return "'" + self.escape_string(obj) + "'"
141-
if isinstance(obj, (bytes, bytearray)):
142-
ret = self._quote_bytes(obj)
143-
return ret
144-
return converters.escape_item(obj, mapping=mapping)
76+
"""Escape whatever value you pass to it."""
77+
return converters.escape_item(obj, mapping)
14578

14679
def escape_string(self, s):
14780
return converters.escape_string(s)
14881

14982
def _quote_bytes(self, s):
15083
return converters.escape_bytes(s)
15184

152-
def _read_query_result(self):
153-
self._result = None
154-
result = CHDBResult(self)
155-
result.read()
156-
self._result = result
157-
return result.affected_rows
158-
15985
def __enter__(self):
16086
"""Context manager that returns a Cursor"""
16187
return self.cursor()
@@ -166,52 +92,9 @@ def __exit__(self, exc, value, traceback):
16692
self.rollback()
16793
else:
16894
self.commit()
95+
self.close()
16996

17097
@property
17198
def resp(self):
99+
"""Return the last query response"""
172100
return self._resp
173-
174-
175-
class CHDBResult(object):
176-
def __init__(self, connection):
177-
"""
178-
:type connection: Connection
179-
"""
180-
self.connection = connection
181-
self.affected_rows = 0
182-
self.insert_id = None
183-
self.warning_count = 0
184-
self.message = None
185-
self.field_count = 0
186-
self.description = None
187-
self.rows = None
188-
self.has_next = None
189-
190-
def read(self):
191-
# Handle empty responses (for instance from CREATE TABLE)
192-
if self.connection.resp is None:
193-
return
194-
195-
try:
196-
data = json.loads(self.connection.resp)
197-
except Exception as error:
198-
raise err.InterfaceError("Unsupported response format:" % error)
199-
200-
try:
201-
self.field_count = len(data["meta"])
202-
description = []
203-
for meta in data["meta"]:
204-
fields = [meta["name"], meta["type"]]
205-
description.append(tuple(fields))
206-
self.description = tuple(description)
207-
208-
rows = []
209-
for line in data["data"]:
210-
row = []
211-
for i in range(self.field_count):
212-
column_data = converters.convert_column_data(self.description[i][1], line[self.description[i][0]])
213-
row.append(column_data)
214-
rows.append(tuple(row))
215-
self.rows = tuple(rows)
216-
except Exception as error:
217-
raise err.InterfaceError("Read return data err:" % error)

0 commit comments

Comments
 (0)