commitdb 1.2.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.
- commitdb/__init__.py +36 -0
- commitdb/binding.py +135 -0
- commitdb/client.py +379 -0
- commitdb/lib/libcommitdb-linux-amd64.so +0 -0
- commitdb-1.2.0.dist-info/METADATA +183 -0
- commitdb-1.2.0.dist-info/RECORD +10 -0
- commitdb-1.2.0.dist-info/WHEEL +5 -0
- commitdb-1.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_client.py +221 -0
commitdb/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CommitDB Python Driver
|
|
3
|
+
|
|
4
|
+
A Python client for connecting to CommitDB SQL Server.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from commitdb import CommitDB
|
|
8
|
+
|
|
9
|
+
db = CommitDB('localhost', 3306)
|
|
10
|
+
db.connect()
|
|
11
|
+
|
|
12
|
+
# Create database and table
|
|
13
|
+
db.execute('CREATE DATABASE mydb')
|
|
14
|
+
db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
|
|
15
|
+
|
|
16
|
+
# Insert data
|
|
17
|
+
db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
|
|
18
|
+
|
|
19
|
+
# Query data
|
|
20
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
21
|
+
for row in result:
|
|
22
|
+
print(row)
|
|
23
|
+
|
|
24
|
+
db.close()
|
|
25
|
+
|
|
26
|
+
Embedded mode (requires libcommitdb):
|
|
27
|
+
from commitdb import CommitDBLocal
|
|
28
|
+
|
|
29
|
+
with CommitDBLocal('/path/to/data') as db:
|
|
30
|
+
db.execute('CREATE DATABASE mydb')
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .client import CommitDB, CommitDBLocal, QueryResult, CommitResult, CommitDBError
|
|
34
|
+
|
|
35
|
+
__version__ = '0.1.0'
|
|
36
|
+
__all__ = ['CommitDB', 'CommitDBLocal', 'QueryResult', 'CommitResult', 'CommitDBError']
|
commitdb/binding.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python ctypes bindings for libcommitdb shared library.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_library() -> Optional[str]:
|
|
14
|
+
"""Find the libcommitdb shared library."""
|
|
15
|
+
system = platform.system()
|
|
16
|
+
|
|
17
|
+
if system == 'Darwin':
|
|
18
|
+
lib_names = ['libcommitdb.dylib', 'libcommitdb-darwin-arm64.dylib', 'libcommitdb-darwin-amd64.dylib']
|
|
19
|
+
elif system == 'Linux':
|
|
20
|
+
lib_names = ['libcommitdb.so', 'libcommitdb-linux-amd64.so', 'libcommitdb-linux-arm64.so']
|
|
21
|
+
elif system == 'Windows':
|
|
22
|
+
lib_names = ['libcommitdb.dll']
|
|
23
|
+
else:
|
|
24
|
+
lib_names = ['libcommitdb.so', 'libcommitdb.dylib']
|
|
25
|
+
|
|
26
|
+
# Search paths
|
|
27
|
+
search_paths = [
|
|
28
|
+
Path(__file__).parent / 'lib', # Package lib directory (for pip installed)
|
|
29
|
+
Path(__file__).parent, # Same directory as this file
|
|
30
|
+
Path(__file__).parent.parent / 'lib', # drivers/python/lib
|
|
31
|
+
Path(__file__).parent.parent.parent.parent / 'lib', # CommitDB/lib
|
|
32
|
+
Path.cwd() / 'lib', # ./lib
|
|
33
|
+
Path.cwd(), # Current directory
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for path in search_paths:
|
|
37
|
+
for lib_name in lib_names:
|
|
38
|
+
lib_path = path / lib_name
|
|
39
|
+
if lib_path.exists():
|
|
40
|
+
return str(lib_path)
|
|
41
|
+
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CommitDBBinding:
|
|
46
|
+
"""Low-level ctypes bindings for libcommitdb."""
|
|
47
|
+
|
|
48
|
+
_lib: Optional[ctypes.CDLL] = None
|
|
49
|
+
_lib_path: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def load(cls, lib_path: Optional[str] = None) -> 'CommitDBBinding':
|
|
53
|
+
"""Load the shared library."""
|
|
54
|
+
if lib_path is None:
|
|
55
|
+
lib_path = _find_library()
|
|
56
|
+
|
|
57
|
+
if lib_path is None:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"Could not find libcommitdb shared library. "
|
|
60
|
+
"Build it with: make lib"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
cls._lib_path = lib_path
|
|
64
|
+
cls._lib = ctypes.CDLL(lib_path)
|
|
65
|
+
|
|
66
|
+
# Define function signatures
|
|
67
|
+
cls._lib.commitdb_open_memory.argtypes = []
|
|
68
|
+
cls._lib.commitdb_open_memory.restype = ctypes.c_int
|
|
69
|
+
|
|
70
|
+
cls._lib.commitdb_open_file.argtypes = [ctypes.c_char_p]
|
|
71
|
+
cls._lib.commitdb_open_file.restype = ctypes.c_int
|
|
72
|
+
|
|
73
|
+
cls._lib.commitdb_close.argtypes = [ctypes.c_int]
|
|
74
|
+
cls._lib.commitdb_close.restype = None
|
|
75
|
+
|
|
76
|
+
# Use c_void_p for the result pointer to avoid automatic conversion
|
|
77
|
+
cls._lib.commitdb_execute.argtypes = [ctypes.c_int, ctypes.c_char_p]
|
|
78
|
+
cls._lib.commitdb_execute.restype = ctypes.c_void_p
|
|
79
|
+
|
|
80
|
+
cls._lib.commitdb_free.argtypes = [ctypes.c_void_p]
|
|
81
|
+
cls._lib.commitdb_free.restype = None
|
|
82
|
+
|
|
83
|
+
return cls()
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def is_loaded(cls) -> bool:
|
|
87
|
+
"""Check if the library is loaded."""
|
|
88
|
+
return cls._lib is not None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def open_memory(cls) -> int:
|
|
92
|
+
"""Open an in-memory database."""
|
|
93
|
+
if not cls.is_loaded():
|
|
94
|
+
cls.load()
|
|
95
|
+
handle = cls._lib.commitdb_open_memory()
|
|
96
|
+
if handle < 0:
|
|
97
|
+
raise RuntimeError("Failed to open in-memory database")
|
|
98
|
+
return handle
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def open_file(cls, path: str) -> int:
|
|
102
|
+
"""Open a file-based database."""
|
|
103
|
+
if not cls.is_loaded():
|
|
104
|
+
cls.load()
|
|
105
|
+
handle = cls._lib.commitdb_open_file(path.encode('utf-8'))
|
|
106
|
+
if handle < 0:
|
|
107
|
+
raise RuntimeError(f"Failed to open database at {path}")
|
|
108
|
+
return handle
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def close(cls, handle: int) -> None:
|
|
112
|
+
"""Close a database handle."""
|
|
113
|
+
if cls.is_loaded():
|
|
114
|
+
cls._lib.commitdb_close(handle)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def execute(cls, handle: int, query: str) -> dict:
|
|
118
|
+
"""Execute a query and return the JSON response."""
|
|
119
|
+
if not cls.is_loaded():
|
|
120
|
+
raise RuntimeError("Library not loaded")
|
|
121
|
+
|
|
122
|
+
result_ptr = cls._lib.commitdb_execute(handle, query.encode('utf-8'))
|
|
123
|
+
if result_ptr is None or result_ptr == 0:
|
|
124
|
+
raise RuntimeError("Query execution failed")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Cast void pointer to char pointer and read the string
|
|
128
|
+
result_str = ctypes.cast(result_ptr, ctypes.c_char_p).value
|
|
129
|
+
if result_str is None:
|
|
130
|
+
raise RuntimeError("Empty response from query")
|
|
131
|
+
result_json = result_str.decode('utf-8')
|
|
132
|
+
return json.loads(result_json)
|
|
133
|
+
finally:
|
|
134
|
+
# Free the allocated memory using the void pointer
|
|
135
|
+
cls._lib.commitdb_free(result_ptr)
|
commitdb/client.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CommitDB Client - Python driver for CommitDB SQL Server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Iterator, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommitDBError(Exception):
|
|
12
|
+
"""Exception raised for CommitDB errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class QueryResult:
|
|
18
|
+
"""Result from a SELECT query."""
|
|
19
|
+
columns: list[str]
|
|
20
|
+
data: list[list[str]]
|
|
21
|
+
records_read: int
|
|
22
|
+
time_ms: float
|
|
23
|
+
|
|
24
|
+
def __iter__(self) -> Iterator[dict[str, str]]:
|
|
25
|
+
"""Iterate over rows as dictionaries."""
|
|
26
|
+
for row in self.data:
|
|
27
|
+
yield dict(zip(self.columns, row))
|
|
28
|
+
|
|
29
|
+
def __len__(self) -> int:
|
|
30
|
+
return len(self.data)
|
|
31
|
+
|
|
32
|
+
def __getitem__(self, index: int) -> dict[str, str]:
|
|
33
|
+
return dict(zip(self.columns, self.data[index]))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CommitResult:
|
|
38
|
+
"""Result from a mutation operation (INSERT, UPDATE, DELETE, CREATE, DROP)."""
|
|
39
|
+
databases_created: int = 0
|
|
40
|
+
databases_deleted: int = 0
|
|
41
|
+
tables_created: int = 0
|
|
42
|
+
tables_deleted: int = 0
|
|
43
|
+
records_written: int = 0
|
|
44
|
+
records_deleted: int = 0
|
|
45
|
+
time_ms: float = 0.0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def affected_rows(self) -> int:
|
|
49
|
+
"""Total number of affected rows/objects."""
|
|
50
|
+
return (self.databases_created + self.databases_deleted +
|
|
51
|
+
self.tables_created + self.tables_deleted +
|
|
52
|
+
self.records_written + self.records_deleted)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CommitDB:
|
|
56
|
+
"""
|
|
57
|
+
CommitDB Python client.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
db = CommitDB('localhost', 3306)
|
|
61
|
+
db.connect()
|
|
62
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
63
|
+
db.close()
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, host: str = 'localhost', port: int = 3306):
|
|
67
|
+
"""
|
|
68
|
+
Initialize CommitDB client.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
host: Server hostname
|
|
72
|
+
port: Server port (default 3306)
|
|
73
|
+
"""
|
|
74
|
+
self.host = host
|
|
75
|
+
self.port = port
|
|
76
|
+
self._socket: Optional[socket.socket] = None
|
|
77
|
+
self._buffer = b''
|
|
78
|
+
|
|
79
|
+
def connect(self, timeout: float = 10.0) -> 'CommitDB':
|
|
80
|
+
"""
|
|
81
|
+
Connect to the CommitDB server.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
timeout: Connection timeout in seconds
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
self for method chaining
|
|
88
|
+
"""
|
|
89
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
90
|
+
self._socket.settimeout(timeout)
|
|
91
|
+
self._socket.connect((self.host, self.port))
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
"""Close the connection."""
|
|
96
|
+
if self._socket:
|
|
97
|
+
try:
|
|
98
|
+
self._socket.send(b'quit\n')
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
self._socket.close()
|
|
102
|
+
self._socket = None
|
|
103
|
+
|
|
104
|
+
def __enter__(self) -> 'CommitDB':
|
|
105
|
+
return self.connect()
|
|
106
|
+
|
|
107
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
108
|
+
self.close()
|
|
109
|
+
|
|
110
|
+
def _send(self, query: str) -> dict:
|
|
111
|
+
"""Send a query and receive the response."""
|
|
112
|
+
if not self._socket:
|
|
113
|
+
raise CommitDBError("Not connected. Call connect() first.")
|
|
114
|
+
|
|
115
|
+
# Send query with newline
|
|
116
|
+
self._socket.send((query + '\n').encode('utf-8'))
|
|
117
|
+
|
|
118
|
+
# Read response until newline
|
|
119
|
+
while b'\n' not in self._buffer:
|
|
120
|
+
chunk = self._socket.recv(4096)
|
|
121
|
+
if not chunk:
|
|
122
|
+
raise CommitDBError("Connection closed by server")
|
|
123
|
+
self._buffer += chunk
|
|
124
|
+
|
|
125
|
+
# Split at first newline
|
|
126
|
+
line, self._buffer = self._buffer.split(b'\n', 1)
|
|
127
|
+
|
|
128
|
+
# Parse JSON response
|
|
129
|
+
try:
|
|
130
|
+
return json.loads(line.decode('utf-8'))
|
|
131
|
+
except json.JSONDecodeError as e:
|
|
132
|
+
raise CommitDBError(f"Invalid response from server: {e}")
|
|
133
|
+
|
|
134
|
+
def execute(self, query: str) -> CommitResult | QueryResult:
|
|
135
|
+
"""
|
|
136
|
+
Execute a SQL query.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
query: SQL query to execute
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
QueryResult for SELECT queries, CommitResult for mutations
|
|
143
|
+
"""
|
|
144
|
+
response = self._send(query)
|
|
145
|
+
|
|
146
|
+
if not response.get('success'):
|
|
147
|
+
raise CommitDBError(response.get('error', 'Unknown error'))
|
|
148
|
+
|
|
149
|
+
result_type = response.get('type')
|
|
150
|
+
result_data = response.get('result', {})
|
|
151
|
+
|
|
152
|
+
if result_type == 'query':
|
|
153
|
+
return QueryResult(
|
|
154
|
+
columns=result_data.get('columns', []),
|
|
155
|
+
data=result_data.get('data', []),
|
|
156
|
+
records_read=result_data.get('records_read', 0),
|
|
157
|
+
time_ms=result_data.get('time_ms', 0.0)
|
|
158
|
+
)
|
|
159
|
+
elif result_type == 'commit':
|
|
160
|
+
return CommitResult(
|
|
161
|
+
databases_created=result_data.get('databases_created', 0),
|
|
162
|
+
databases_deleted=result_data.get('databases_deleted', 0),
|
|
163
|
+
tables_created=result_data.get('tables_created', 0),
|
|
164
|
+
tables_deleted=result_data.get('tables_deleted', 0),
|
|
165
|
+
records_written=result_data.get('records_written', 0),
|
|
166
|
+
records_deleted=result_data.get('records_deleted', 0),
|
|
167
|
+
time_ms=result_data.get('time_ms', 0.0)
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
# Unknown type, return empty commit result
|
|
171
|
+
return CommitResult()
|
|
172
|
+
|
|
173
|
+
def query(self, sql: str) -> QueryResult:
|
|
174
|
+
"""
|
|
175
|
+
Execute a SELECT query and return results.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
sql: SELECT query
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
QueryResult with columns and data
|
|
182
|
+
"""
|
|
183
|
+
result = self.execute(sql)
|
|
184
|
+
if not isinstance(result, QueryResult):
|
|
185
|
+
raise CommitDBError("Expected query result, got commit result")
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
def create_database(self, name: str) -> CommitResult:
|
|
189
|
+
"""Create a database."""
|
|
190
|
+
result = self.execute(f'CREATE DATABASE {name}')
|
|
191
|
+
if not isinstance(result, CommitResult):
|
|
192
|
+
raise CommitDBError("Expected commit result")
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def drop_database(self, name: str) -> CommitResult:
|
|
196
|
+
"""Drop a database."""
|
|
197
|
+
result = self.execute(f'DROP DATABASE {name}')
|
|
198
|
+
if not isinstance(result, CommitResult):
|
|
199
|
+
raise CommitDBError("Expected commit result")
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def create_table(self, database: str, table: str, columns: str) -> CommitResult:
|
|
203
|
+
"""
|
|
204
|
+
Create a table.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
database: Database name
|
|
208
|
+
table: Table name
|
|
209
|
+
columns: Column definitions, e.g. "id INT PRIMARY KEY, name STRING"
|
|
210
|
+
"""
|
|
211
|
+
result = self.execute(f'CREATE TABLE {database}.{table} ({columns})')
|
|
212
|
+
if not isinstance(result, CommitResult):
|
|
213
|
+
raise CommitDBError("Expected commit result")
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
def insert(self, database: str, table: str, columns: list[str], values: list) -> CommitResult:
|
|
217
|
+
"""
|
|
218
|
+
Insert a row.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
database: Database name
|
|
222
|
+
table: Table name
|
|
223
|
+
columns: List of column names
|
|
224
|
+
values: List of values (strings will be quoted)
|
|
225
|
+
"""
|
|
226
|
+
cols = ', '.join(columns)
|
|
227
|
+
vals = ', '.join(
|
|
228
|
+
f"'{v}'" if isinstance(v, str) else str(v)
|
|
229
|
+
for v in values
|
|
230
|
+
)
|
|
231
|
+
result = self.execute(f'INSERT INTO {database}.{table} ({cols}) VALUES ({vals})')
|
|
232
|
+
if not isinstance(result, CommitResult):
|
|
233
|
+
raise CommitDBError("Expected commit result")
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
def show_databases(self) -> list[str]:
|
|
237
|
+
"""List all databases."""
|
|
238
|
+
result = self.query('SHOW DATABASES')
|
|
239
|
+
return [row[0] for row in result.data] if result.data else []
|
|
240
|
+
|
|
241
|
+
def show_tables(self, database: str) -> list[str]:
|
|
242
|
+
"""List all tables in a database."""
|
|
243
|
+
result = self.query(f'SHOW TABLES IN {database}')
|
|
244
|
+
return [row[0] for row in result.data] if result.data else []
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class CommitDBLocal:
|
|
248
|
+
"""
|
|
249
|
+
CommitDB embedded client using Go bindings.
|
|
250
|
+
|
|
251
|
+
This mode runs the database engine directly in-process without
|
|
252
|
+
requiring a separate server.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
# In-memory database
|
|
256
|
+
db = CommitDBLocal()
|
|
257
|
+
|
|
258
|
+
# File-based database
|
|
259
|
+
db = CommitDBLocal('/path/to/data')
|
|
260
|
+
|
|
261
|
+
db.execute('CREATE DATABASE mydb')
|
|
262
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
263
|
+
db.close()
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, path: Optional[str] = None, lib_path: Optional[str] = None):
|
|
267
|
+
"""
|
|
268
|
+
Initialize CommitDB embedded client.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
path: Path for file-based persistence. If None, uses in-memory storage.
|
|
272
|
+
lib_path: Optional path to libcommitdb shared library.
|
|
273
|
+
"""
|
|
274
|
+
from .binding import CommitDBBinding
|
|
275
|
+
|
|
276
|
+
self._binding = CommitDBBinding
|
|
277
|
+
if lib_path:
|
|
278
|
+
self._binding.load(lib_path)
|
|
279
|
+
|
|
280
|
+
self._path = path
|
|
281
|
+
self._handle: Optional[int] = None
|
|
282
|
+
|
|
283
|
+
def open(self) -> 'CommitDBLocal':
|
|
284
|
+
"""Open the database."""
|
|
285
|
+
if self._path:
|
|
286
|
+
self._handle = self._binding.open_file(self._path)
|
|
287
|
+
else:
|
|
288
|
+
self._handle = self._binding.open_memory()
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def close(self) -> None:
|
|
292
|
+
"""Close the database."""
|
|
293
|
+
if self._handle is not None:
|
|
294
|
+
self._binding.close(self._handle)
|
|
295
|
+
self._handle = None
|
|
296
|
+
|
|
297
|
+
def __enter__(self) -> 'CommitDBLocal':
|
|
298
|
+
return self.open()
|
|
299
|
+
|
|
300
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
301
|
+
self.close()
|
|
302
|
+
|
|
303
|
+
def _parse_response(self, response: dict) -> CommitResult | QueryResult:
|
|
304
|
+
"""Parse a response dict into result objects."""
|
|
305
|
+
if not response.get('success'):
|
|
306
|
+
raise CommitDBError(response.get('error', 'Unknown error'))
|
|
307
|
+
|
|
308
|
+
result_type = response.get('type')
|
|
309
|
+
result_data = response.get('result', {})
|
|
310
|
+
|
|
311
|
+
if result_type == 'query':
|
|
312
|
+
return QueryResult(
|
|
313
|
+
columns=result_data.get('columns', []),
|
|
314
|
+
data=result_data.get('data', []),
|
|
315
|
+
records_read=result_data.get('records_read', 0),
|
|
316
|
+
time_ms=result_data.get('time_ms', 0.0)
|
|
317
|
+
)
|
|
318
|
+
elif result_type == 'commit':
|
|
319
|
+
return CommitResult(
|
|
320
|
+
databases_created=result_data.get('databases_created', 0),
|
|
321
|
+
databases_deleted=result_data.get('databases_deleted', 0),
|
|
322
|
+
tables_created=result_data.get('tables_created', 0),
|
|
323
|
+
tables_deleted=result_data.get('tables_deleted', 0),
|
|
324
|
+
records_written=result_data.get('records_written', 0),
|
|
325
|
+
records_deleted=result_data.get('records_deleted', 0),
|
|
326
|
+
time_ms=result_data.get('time_ms', 0.0)
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
return CommitResult()
|
|
330
|
+
|
|
331
|
+
def execute(self, query: str) -> CommitResult | QueryResult:
|
|
332
|
+
"""
|
|
333
|
+
Execute a SQL query.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
query: SQL query to execute
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
QueryResult for SELECT queries, CommitResult for mutations
|
|
340
|
+
"""
|
|
341
|
+
if self._handle is None:
|
|
342
|
+
raise CommitDBError("Database not open. Call open() first.")
|
|
343
|
+
|
|
344
|
+
response = self._binding.execute(self._handle, query)
|
|
345
|
+
return self._parse_response(response)
|
|
346
|
+
|
|
347
|
+
def query(self, sql: str) -> QueryResult:
|
|
348
|
+
"""Execute a SELECT query and return results."""
|
|
349
|
+
result = self.execute(sql)
|
|
350
|
+
if not isinstance(result, QueryResult):
|
|
351
|
+
raise CommitDBError("Expected query result, got commit result")
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
def create_database(self, name: str) -> CommitResult:
|
|
355
|
+
"""Create a database."""
|
|
356
|
+
result = self.execute(f'CREATE DATABASE {name}')
|
|
357
|
+
if not isinstance(result, CommitResult):
|
|
358
|
+
raise CommitDBError("Expected commit result")
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def create_table(self, database: str, table: str, columns: str) -> CommitResult:
|
|
362
|
+
"""Create a table."""
|
|
363
|
+
result = self.execute(f'CREATE TABLE {database}.{table} ({columns})')
|
|
364
|
+
if not isinstance(result, CommitResult):
|
|
365
|
+
raise CommitDBError("Expected commit result")
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def insert(self, database: str, table: str, columns: list[str], values: list) -> CommitResult:
|
|
369
|
+
"""Insert a row."""
|
|
370
|
+
cols = ', '.join(columns)
|
|
371
|
+
vals = ', '.join(
|
|
372
|
+
f"'{v}'" if isinstance(v, str) else str(v)
|
|
373
|
+
for v in values
|
|
374
|
+
)
|
|
375
|
+
result = self.execute(f'INSERT INTO {database}.{table} ({cols}) VALUES ({vals})')
|
|
376
|
+
if not isinstance(result, CommitResult):
|
|
377
|
+
raise CommitDBError("Expected commit result")
|
|
378
|
+
return result
|
|
379
|
+
|
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commitdb
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Python driver for CommitDB SQL Server
|
|
5
|
+
Author: CommitDB Contributors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/nickyhof/CommitDB
|
|
8
|
+
Project-URL: Documentation, https://github.com/nickyhof/CommitDB#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/nickyhof/CommitDB
|
|
10
|
+
Keywords: database,sql,git,commitdb
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# CommitDB Python Driver
|
|
24
|
+
|
|
25
|
+
Python client for connecting to CommitDB SQL Server.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e drivers/python
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from commitdb import CommitDB
|
|
37
|
+
|
|
38
|
+
# Connect to server
|
|
39
|
+
db = CommitDB('localhost', 3306)
|
|
40
|
+
db.connect()
|
|
41
|
+
|
|
42
|
+
# Or use context manager
|
|
43
|
+
with CommitDB('localhost', 3306) as db:
|
|
44
|
+
# Create database and table
|
|
45
|
+
db.execute('CREATE DATABASE mydb')
|
|
46
|
+
db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
|
|
47
|
+
|
|
48
|
+
# Insert data
|
|
49
|
+
db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
|
|
50
|
+
db.execute("INSERT INTO mydb.users (id, name) VALUES (2, 'Bob')")
|
|
51
|
+
|
|
52
|
+
# Query data
|
|
53
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
54
|
+
print(f"Columns: {result.columns}")
|
|
55
|
+
print(f"Rows: {len(result)}")
|
|
56
|
+
|
|
57
|
+
# Iterate over rows as dictionaries
|
|
58
|
+
for row in result:
|
|
59
|
+
print(f" {row['id']}: {row['name']}")
|
|
60
|
+
|
|
61
|
+
# Use convenience methods
|
|
62
|
+
db.insert('mydb', 'users', ['id', 'name'], [3, 'Charlie'])
|
|
63
|
+
tables = db.show_tables('mydb')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### CommitDB
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
CommitDB(host='localhost', port=3306)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Methods:**
|
|
75
|
+
|
|
76
|
+
- `connect(timeout=10.0)` - Connect to server
|
|
77
|
+
- `close()` - Close connection
|
|
78
|
+
- `execute(sql)` - Execute any SQL query
|
|
79
|
+
- `query(sql)` - Execute SELECT query, returns QueryResult
|
|
80
|
+
- `create_database(name)` - Create a database
|
|
81
|
+
- `drop_database(name)` - Drop a database
|
|
82
|
+
- `create_table(database, table, columns)` - Create a table
|
|
83
|
+
- `insert(database, table, columns, values)` - Insert a row
|
|
84
|
+
- `show_databases()` - List databases
|
|
85
|
+
- `show_tables(database)` - List tables
|
|
86
|
+
|
|
87
|
+
### QueryResult
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
91
|
+
result.columns # ['id', 'name']
|
|
92
|
+
result.data # [['1', 'Alice'], ['2', 'Bob']]
|
|
93
|
+
len(result) # 2
|
|
94
|
+
result[0] # {'id': '1', 'name': 'Alice'}
|
|
95
|
+
for row in result:
|
|
96
|
+
print(row) # {'id': '1', 'name': 'Alice'}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### CommitResult
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
result = db.execute('INSERT INTO mydb.users ...')
|
|
103
|
+
result.records_written # 1
|
|
104
|
+
result.affected_rows # 1
|
|
105
|
+
result.time_ms # 1.5
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Embedded Mode (CommitDBLocal)
|
|
109
|
+
|
|
110
|
+
Run CommitDB directly in-process without a separate server. Requires the `libcommitdb` shared library.
|
|
111
|
+
|
|
112
|
+
### Quick Start
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from commitdb import CommitDBLocal
|
|
116
|
+
|
|
117
|
+
# In-memory database (no persistence)
|
|
118
|
+
with CommitDBLocal() as db:
|
|
119
|
+
db.execute('CREATE DATABASE mydb')
|
|
120
|
+
db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
|
|
121
|
+
db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
|
|
122
|
+
result = db.query('SELECT * FROM mydb.users')
|
|
123
|
+
|
|
124
|
+
# File-based persistence
|
|
125
|
+
with CommitDBLocal('/path/to/data') as db:
|
|
126
|
+
db.execute('CREATE DATABASE mydb')
|
|
127
|
+
# Data persists between sessions
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Building the Shared Library
|
|
131
|
+
|
|
132
|
+
If the library isn't bundled with your pip install:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# From CommitDB root
|
|
136
|
+
make lib # Creates lib/libcommitdb.dylib (macOS) or .so (Linux)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### CommitDBLocal API
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
CommitDBLocal(path=None, lib_path=None)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
- `path` - Directory for file-based persistence. If `None`, uses in-memory storage.
|
|
147
|
+
- `lib_path` - Optional path to `libcommitdb` shared library.
|
|
148
|
+
|
|
149
|
+
**Methods:**
|
|
150
|
+
- `open()` - Open the database
|
|
151
|
+
- `close()` - Close the database
|
|
152
|
+
- `execute(sql)` - Execute any SQL query
|
|
153
|
+
- `query(sql)` - Execute SELECT query
|
|
154
|
+
- `create_database(name)` - Create a database
|
|
155
|
+
- `create_table(database, table, columns)` - Create a table
|
|
156
|
+
- `insert(database, table, columns, values)` - Insert a row
|
|
157
|
+
|
|
158
|
+
### Example: Full CRUD
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from commitdb import CommitDBLocal
|
|
162
|
+
|
|
163
|
+
with CommitDBLocal() as db:
|
|
164
|
+
# Create
|
|
165
|
+
db.create_database('app')
|
|
166
|
+
db.create_table('app', 'users', 'id INT PRIMARY KEY, name STRING, email STRING')
|
|
167
|
+
|
|
168
|
+
# Insert
|
|
169
|
+
db.insert('app', 'users', ['id', 'name', 'email'], [1, 'Alice', 'alice@example.com'])
|
|
170
|
+
db.insert('app', 'users', ['id', 'name', 'email'], [2, 'Bob', 'bob@example.com'])
|
|
171
|
+
|
|
172
|
+
# Read
|
|
173
|
+
result = db.query('SELECT * FROM app.users')
|
|
174
|
+
for row in result:
|
|
175
|
+
print(f"{row['name']}: {row['email']}")
|
|
176
|
+
|
|
177
|
+
# Update
|
|
178
|
+
db.execute("UPDATE app.users SET email = 'alice@new.com' WHERE id = 1")
|
|
179
|
+
|
|
180
|
+
# Delete
|
|
181
|
+
db.execute('DELETE FROM app.users WHERE id = 2')
|
|
182
|
+
```
|
|
183
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
commitdb/__init__.py,sha256=A0vyiCY3du55mA9914aRAd9PoAQMM3SrcBf_Ek3aNQs,916
|
|
2
|
+
commitdb/binding.py,sha256=_PaafsoHxGgpZSOd27FkymM4x6BhwK07XZPXVxjvALU,4585
|
|
3
|
+
commitdb/client.py,sha256=PZvPAnKUEefzJmGJhoG-qYy9g91kPYQB-G1PGQ7_a3s,12465
|
|
4
|
+
commitdb/lib/libcommitdb-linux-amd64.so,sha256=AOblBS3zyXqMLyjxYFzYcyH00xc-z6-T1o4hjNgeIz8,14729328
|
|
5
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
tests/test_client.py,sha256=c4L72UbKP5sBg2AHerJ5jJchWL7jE89JBga4MlQVfDQ,7385
|
|
7
|
+
commitdb-1.2.0.dist-info/METADATA,sha256=8_tiO1o0IL4suMHzvSVh5O7ACqKnDnMsqdCJb4HbnHo,5041
|
|
8
|
+
commitdb-1.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
9
|
+
commitdb-1.2.0.dist-info/top_level.txt,sha256=hxbEo9CCoDzsT0VaAiV1QViidkmyaBJtUDtCFOBH-t8,15
|
|
10
|
+
commitdb-1.2.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_client.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for CommitDB Python driver.
|
|
3
|
+
|
|
4
|
+
To run with a live server:
|
|
5
|
+
1. Start the server: go run ./cmd/server
|
|
6
|
+
2. Run tests: pytest drivers/python/tests/
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from commitdb import CommitDB, QueryResult, CommitResult, CommitDBError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestQueryResult:
|
|
14
|
+
"""Tests for QueryResult class."""
|
|
15
|
+
|
|
16
|
+
def test_iteration(self):
|
|
17
|
+
result = QueryResult(
|
|
18
|
+
columns=['id', 'name'],
|
|
19
|
+
data=[['1', 'Alice'], ['2', 'Bob']],
|
|
20
|
+
records_read=2,
|
|
21
|
+
time_ms=1.0
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
rows = list(result)
|
|
25
|
+
assert rows == [
|
|
26
|
+
{'id': '1', 'name': 'Alice'},
|
|
27
|
+
{'id': '2', 'name': 'Bob'}
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def test_len(self):
|
|
31
|
+
result = QueryResult(
|
|
32
|
+
columns=['id'],
|
|
33
|
+
data=[['1'], ['2'], ['3']],
|
|
34
|
+
records_read=3,
|
|
35
|
+
time_ms=1.0
|
|
36
|
+
)
|
|
37
|
+
assert len(result) == 3
|
|
38
|
+
|
|
39
|
+
def test_getitem(self):
|
|
40
|
+
result = QueryResult(
|
|
41
|
+
columns=['id', 'name'],
|
|
42
|
+
data=[['1', 'Alice'], ['2', 'Bob']],
|
|
43
|
+
records_read=2,
|
|
44
|
+
time_ms=1.0
|
|
45
|
+
)
|
|
46
|
+
assert result[0] == {'id': '1', 'name': 'Alice'}
|
|
47
|
+
assert result[1] == {'id': '2', 'name': 'Bob'}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestCommitResult:
|
|
51
|
+
"""Tests for CommitResult class."""
|
|
52
|
+
|
|
53
|
+
def test_affected_rows(self):
|
|
54
|
+
result = CommitResult(
|
|
55
|
+
databases_created=1,
|
|
56
|
+
tables_created=2,
|
|
57
|
+
records_written=3
|
|
58
|
+
)
|
|
59
|
+
assert result.affected_rows == 6
|
|
60
|
+
|
|
61
|
+
def test_defaults(self):
|
|
62
|
+
result = CommitResult()
|
|
63
|
+
assert result.affected_rows == 0
|
|
64
|
+
assert result.time_ms == 0.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestCommitDBUnit:
|
|
68
|
+
"""Unit tests for CommitDB client (no server required)."""
|
|
69
|
+
|
|
70
|
+
def test_init(self):
|
|
71
|
+
db = CommitDB('localhost', 3306)
|
|
72
|
+
assert db.host == 'localhost'
|
|
73
|
+
assert db.port == 3306
|
|
74
|
+
|
|
75
|
+
def test_not_connected_error(self):
|
|
76
|
+
db = CommitDB('localhost', 3306)
|
|
77
|
+
with pytest.raises(CommitDBError, match="Not connected"):
|
|
78
|
+
db.execute("SELECT 1")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Integration tests require a running server
|
|
82
|
+
# These run automatically in CI where the server is started
|
|
83
|
+
|
|
84
|
+
import os
|
|
85
|
+
SKIP_INTEGRATION = os.environ.get('COMMITDB_SERVER_URL') is None and os.environ.get('CI') is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.skipif(SKIP_INTEGRATION, reason="Server not running - set COMMITDB_SERVER_URL or CI env var")
|
|
89
|
+
class TestCommitDBIntegration:
|
|
90
|
+
"""Integration tests (requires running server)."""
|
|
91
|
+
|
|
92
|
+
@pytest.fixture
|
|
93
|
+
def db(self):
|
|
94
|
+
host = os.environ.get('COMMITDB_HOST', 'localhost')
|
|
95
|
+
port = int(os.environ.get('COMMITDB_PORT', '3306'))
|
|
96
|
+
db = CommitDB(host, port)
|
|
97
|
+
db.connect()
|
|
98
|
+
yield db
|
|
99
|
+
db.close()
|
|
100
|
+
|
|
101
|
+
def test_create_database(self, db):
|
|
102
|
+
result = db.execute('CREATE DATABASE pytest_int_test1')
|
|
103
|
+
assert isinstance(result, CommitResult)
|
|
104
|
+
assert result.databases_created == 1
|
|
105
|
+
|
|
106
|
+
def test_create_table(self, db):
|
|
107
|
+
db.execute('CREATE DATABASE pytest_int_test2')
|
|
108
|
+
result = db.execute('CREATE TABLE pytest_int_test2.users (id INT PRIMARY KEY, name STRING)')
|
|
109
|
+
assert isinstance(result, CommitResult)
|
|
110
|
+
assert result.tables_created == 1
|
|
111
|
+
|
|
112
|
+
def test_insert_and_query(self, db):
|
|
113
|
+
db.execute('CREATE DATABASE pytest_int_test3')
|
|
114
|
+
db.execute('CREATE TABLE pytest_int_test3.items (id INT PRIMARY KEY, value STRING)')
|
|
115
|
+
db.execute("INSERT INTO pytest_int_test3.items (id, value) VALUES (1, 'hello')")
|
|
116
|
+
|
|
117
|
+
result = db.query('SELECT * FROM pytest_int_test3.items')
|
|
118
|
+
assert isinstance(result, QueryResult)
|
|
119
|
+
assert len(result) == 1
|
|
120
|
+
assert result[0] == {'id': '1', 'value': 'hello'}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Embedded mode tests (require libcommitdb shared library)
|
|
126
|
+
# Run with: make lib && pytest drivers/python/tests/ -v
|
|
127
|
+
|
|
128
|
+
import os
|
|
129
|
+
from pathlib import Path
|
|
130
|
+
|
|
131
|
+
# Try to find the shared library
|
|
132
|
+
def _find_lib():
|
|
133
|
+
lib_paths = [
|
|
134
|
+
Path(__file__).parent.parent.parent.parent.parent / 'lib' / 'libcommitdb.dylib',
|
|
135
|
+
Path(__file__).parent.parent.parent.parent.parent / 'lib' / 'libcommitdb.so',
|
|
136
|
+
]
|
|
137
|
+
for p in lib_paths:
|
|
138
|
+
if p.exists():
|
|
139
|
+
return str(p)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
LIB_PATH = _find_lib()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.skipif(LIB_PATH is None, reason="libcommitdb not found - run 'make lib' first")
|
|
146
|
+
class TestCommitDBLocal:
|
|
147
|
+
"""Tests for embedded mode using Go bindings."""
|
|
148
|
+
|
|
149
|
+
@pytest.fixture
|
|
150
|
+
def db(self):
|
|
151
|
+
from commitdb import CommitDBLocal
|
|
152
|
+
db = CommitDBLocal(lib_path=LIB_PATH)
|
|
153
|
+
db.open()
|
|
154
|
+
yield db
|
|
155
|
+
db.close()
|
|
156
|
+
|
|
157
|
+
def test_create_database(self, db):
|
|
158
|
+
result = db.execute('CREATE DATABASE local_test1')
|
|
159
|
+
assert isinstance(result, CommitResult)
|
|
160
|
+
assert result.databases_created == 1
|
|
161
|
+
|
|
162
|
+
def test_create_table(self, db):
|
|
163
|
+
db.execute('CREATE DATABASE local_test2')
|
|
164
|
+
result = db.execute('CREATE TABLE local_test2.users (id INT PRIMARY KEY, name STRING)')
|
|
165
|
+
assert isinstance(result, CommitResult)
|
|
166
|
+
assert result.tables_created == 1
|
|
167
|
+
|
|
168
|
+
def test_insert_and_query(self, db):
|
|
169
|
+
db.execute('CREATE DATABASE local_test3')
|
|
170
|
+
db.execute('CREATE TABLE local_test3.items (id INT PRIMARY KEY, value STRING)')
|
|
171
|
+
db.execute("INSERT INTO local_test3.items (id, value) VALUES (1, 'hello')")
|
|
172
|
+
db.execute("INSERT INTO local_test3.items (id, value) VALUES (2, 'world')")
|
|
173
|
+
|
|
174
|
+
result = db.query('SELECT * FROM local_test3.items')
|
|
175
|
+
assert isinstance(result, QueryResult)
|
|
176
|
+
assert len(result) == 2
|
|
177
|
+
assert result[0] == {'id': '1', 'value': 'hello'}
|
|
178
|
+
assert result[1] == {'id': '2', 'value': 'world'}
|
|
179
|
+
|
|
180
|
+
def test_update(self, db):
|
|
181
|
+
db.execute('CREATE DATABASE local_test4')
|
|
182
|
+
db.execute('CREATE TABLE local_test4.data (id INT PRIMARY KEY, val STRING)')
|
|
183
|
+
db.execute("INSERT INTO local_test4.data (id, val) VALUES (1, 'old')")
|
|
184
|
+
|
|
185
|
+
result = db.execute("UPDATE local_test4.data SET val = 'new' WHERE id = 1")
|
|
186
|
+
assert isinstance(result, CommitResult)
|
|
187
|
+
|
|
188
|
+
result = db.query('SELECT * FROM local_test4.data WHERE id = 1')
|
|
189
|
+
assert result[0]['val'] == 'new'
|
|
190
|
+
|
|
191
|
+
def test_delete(self, db):
|
|
192
|
+
db.execute('CREATE DATABASE local_test5')
|
|
193
|
+
db.execute('CREATE TABLE local_test5.data (id INT PRIMARY KEY)')
|
|
194
|
+
db.execute('INSERT INTO local_test5.data (id) VALUES (1)')
|
|
195
|
+
db.execute('INSERT INTO local_test5.data (id) VALUES (2)')
|
|
196
|
+
|
|
197
|
+
db.execute('DELETE FROM local_test5.data WHERE id = 1')
|
|
198
|
+
|
|
199
|
+
result = db.query('SELECT * FROM local_test5.data')
|
|
200
|
+
assert len(result) == 1
|
|
201
|
+
assert result[0]['id'] == '2'
|
|
202
|
+
|
|
203
|
+
def test_context_manager(self):
|
|
204
|
+
from commitdb import CommitDBLocal
|
|
205
|
+
with CommitDBLocal(lib_path=LIB_PATH) as db:
|
|
206
|
+
result = db.execute('CREATE DATABASE local_test6')
|
|
207
|
+
assert result.databases_created == 1
|
|
208
|
+
|
|
209
|
+
def test_convenience_methods(self, db):
|
|
210
|
+
db.create_database('local_test7')
|
|
211
|
+
db.create_table('local_test7', 'users', 'id INT PRIMARY KEY, name STRING')
|
|
212
|
+
db.insert('local_test7', 'users', ['id', 'name'], [1, 'Alice'])
|
|
213
|
+
|
|
214
|
+
result = db.query('SELECT * FROM local_test7.users')
|
|
215
|
+
assert len(result) == 1
|
|
216
|
+
assert result[0] == {'id': '1', 'name': 'Alice'}
|
|
217
|
+
|
|
218
|
+
def test_error_handling(self, db):
|
|
219
|
+
with pytest.raises(CommitDBError):
|
|
220
|
+
db.query('SELECT * FROM nonexistent.table')
|
|
221
|
+
|