tmjlite 0.2.8__py3-none-win_amd64.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.
- tmjlite/__init__.py +37 -0
- tmjlite/_lib/win32-x64/tmjlite_ffi.dll +0 -0
- tmjlite/driver.py +595 -0
- tmjlite-0.2.8.dist-info/METADATA +73 -0
- tmjlite-0.2.8.dist-info/RECORD +7 -0
- tmjlite-0.2.8.dist-info/WHEEL +5 -0
- tmjlite-0.2.8.dist-info/top_level.txt +1 -0
tmjlite/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TMJLite Python Driver
|
|
3
|
+
Cross-platform embedded database engine
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
import tmjlite
|
|
7
|
+
|
|
8
|
+
db = tmjlite.connect("mydb.tmjp")
|
|
9
|
+
db.execute("CREATE TABLE users (name TEXT, age INT);")
|
|
10
|
+
db.execute("INSERT INTO users VALUES ('Alice', 30);")
|
|
11
|
+
|
|
12
|
+
for row in db.query("SELECT * FROM users;"):
|
|
13
|
+
print(row["name"], row["age"])
|
|
14
|
+
|
|
15
|
+
with db.transaction():
|
|
16
|
+
db.execute("INSERT INTO users VALUES ('Bob', 25);")
|
|
17
|
+
|
|
18
|
+
db.close()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from tmjlite.driver import (
|
|
22
|
+
connect,
|
|
23
|
+
connect_shadow,
|
|
24
|
+
connect_shared,
|
|
25
|
+
open_branch,
|
|
26
|
+
Connection,
|
|
27
|
+
QueryResult,
|
|
28
|
+
Row,
|
|
29
|
+
ShadowConnection,
|
|
30
|
+
SharedConnection,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.8"
|
|
34
|
+
__all__ = [
|
|
35
|
+
"connect", "connect_shared", "connect_shadow", "open_branch",
|
|
36
|
+
"Connection", "SharedConnection", "ShadowConnection", "QueryResult", "Row",
|
|
37
|
+
]
|
|
Binary file
|
tmjlite/driver.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TMJLite Python driver — loads libtmjlite_ffi via ctypes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_library():
|
|
14
|
+
"""Locate the TMJLite shared library."""
|
|
15
|
+
system = platform.system()
|
|
16
|
+
if system == "Darwin":
|
|
17
|
+
name = "libtmjlite_ffi.dylib"
|
|
18
|
+
elif system == "Linux":
|
|
19
|
+
name = "libtmjlite_ffi.so"
|
|
20
|
+
elif system == "Windows":
|
|
21
|
+
name = "tmjlite_ffi.dll"
|
|
22
|
+
else:
|
|
23
|
+
raise OSError(f"unsupported platform: {system}")
|
|
24
|
+
|
|
25
|
+
search_paths = []
|
|
26
|
+
|
|
27
|
+
env_path = os.environ.get("TMJLITE_LIB_PATH")
|
|
28
|
+
if env_path:
|
|
29
|
+
search_paths.append(Path(env_path))
|
|
30
|
+
|
|
31
|
+
driver_dir = Path(__file__).resolve().parent
|
|
32
|
+
|
|
33
|
+
# Lib nativa embutida no pacote (_lib/<os>-<arch>/)
|
|
34
|
+
machine = platform.machine().lower()
|
|
35
|
+
arch = "arm64" if machine in ("arm64", "aarch64") else \
|
|
36
|
+
"x64" if machine in ("x86_64", "amd64") else machine
|
|
37
|
+
os_slug = {"Darwin": "darwin", "Linux": "linux", "Windows": "win32"}.get(system, system.lower())
|
|
38
|
+
search_paths.append(driver_dir / "_lib" / f"{os_slug}-{arch}" / name)
|
|
39
|
+
|
|
40
|
+
search_paths.extend([
|
|
41
|
+
driver_dir / name,
|
|
42
|
+
driver_dir.parent / name,
|
|
43
|
+
driver_dir.parent / "lib" / name,
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
repo_root = driver_dir.parent.parent.parent
|
|
47
|
+
search_paths.extend([
|
|
48
|
+
repo_root / "target" / "release" / name,
|
|
49
|
+
repo_root / "target" / "debug" / name,
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
for p in search_paths:
|
|
53
|
+
if p.is_file():
|
|
54
|
+
return str(p)
|
|
55
|
+
|
|
56
|
+
raise OSError(
|
|
57
|
+
f"cannot find {name}. Set TMJLITE_LIB_PATH or build with: "
|
|
58
|
+
f"cargo build --release -p tmjlite-ffi"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_lib():
|
|
63
|
+
path = _find_library()
|
|
64
|
+
lib = ctypes.cdll.LoadLibrary(path)
|
|
65
|
+
|
|
66
|
+
# Lifecycle
|
|
67
|
+
lib.tmjlite_open.argtypes = [ctypes.c_char_p]
|
|
68
|
+
lib.tmjlite_open.restype = ctypes.c_void_p
|
|
69
|
+
|
|
70
|
+
lib.tmjlite_open_memory.argtypes = []
|
|
71
|
+
lib.tmjlite_open_memory.restype = ctypes.c_void_p
|
|
72
|
+
|
|
73
|
+
lib.tmjlite_close.argtypes = [ctypes.c_void_p]
|
|
74
|
+
lib.tmjlite_close.restype = None
|
|
75
|
+
|
|
76
|
+
# Execute
|
|
77
|
+
lib.tmjlite_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
78
|
+
lib.tmjlite_exec.restype = ctypes.c_int
|
|
79
|
+
|
|
80
|
+
# Query
|
|
81
|
+
lib.tmjlite_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
82
|
+
lib.tmjlite_query.restype = ctypes.c_void_p
|
|
83
|
+
|
|
84
|
+
# Result accessors
|
|
85
|
+
lib.tmjlite_result_column_count.argtypes = [ctypes.c_void_p]
|
|
86
|
+
lib.tmjlite_result_column_count.restype = ctypes.c_int
|
|
87
|
+
|
|
88
|
+
lib.tmjlite_result_column_name.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
89
|
+
lib.tmjlite_result_column_name.restype = ctypes.c_char_p
|
|
90
|
+
|
|
91
|
+
lib.tmjlite_result_row_count.argtypes = [ctypes.c_void_p]
|
|
92
|
+
lib.tmjlite_result_row_count.restype = ctypes.c_int
|
|
93
|
+
|
|
94
|
+
lib.tmjlite_result_value.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
|
|
95
|
+
lib.tmjlite_result_value.restype = ctypes.c_char_p
|
|
96
|
+
|
|
97
|
+
lib.tmjlite_result_message.argtypes = [ctypes.c_void_p]
|
|
98
|
+
lib.tmjlite_result_message.restype = ctypes.c_char_p
|
|
99
|
+
|
|
100
|
+
lib.tmjlite_result_free.argtypes = [ctypes.c_void_p]
|
|
101
|
+
lib.tmjlite_result_free.restype = None
|
|
102
|
+
|
|
103
|
+
# Error
|
|
104
|
+
lib.tmjlite_errmsg.argtypes = [ctypes.c_void_p]
|
|
105
|
+
lib.tmjlite_errmsg.restype = ctypes.c_char_p
|
|
106
|
+
|
|
107
|
+
# Save
|
|
108
|
+
lib.tmjlite_save.argtypes = [ctypes.c_void_p]
|
|
109
|
+
lib.tmjlite_save.restype = ctypes.c_int
|
|
110
|
+
|
|
111
|
+
lib.tmjlite_save_as.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
112
|
+
lib.tmjlite_save_as.restype = ctypes.c_int
|
|
113
|
+
|
|
114
|
+
# Shared (thread-safe) API
|
|
115
|
+
lib.tmjlite_open_shared.argtypes = [ctypes.c_char_p]
|
|
116
|
+
lib.tmjlite_open_shared.restype = ctypes.c_void_p
|
|
117
|
+
|
|
118
|
+
lib.tmjlite_close_shared.argtypes = [ctypes.c_void_p]
|
|
119
|
+
lib.tmjlite_close_shared.restype = None
|
|
120
|
+
|
|
121
|
+
lib.tmjlite_exec_shared.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
122
|
+
lib.tmjlite_exec_shared.restype = ctypes.c_int
|
|
123
|
+
|
|
124
|
+
lib.tmjlite_query_shared.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
125
|
+
lib.tmjlite_query_shared.restype = ctypes.c_void_p
|
|
126
|
+
|
|
127
|
+
lib.tmjlite_errmsg_shared.argtypes = [ctypes.c_void_p]
|
|
128
|
+
lib.tmjlite_errmsg_shared.restype = ctypes.c_char_p
|
|
129
|
+
|
|
130
|
+
# Version
|
|
131
|
+
lib.tmjlite_version.argtypes = []
|
|
132
|
+
lib.tmjlite_version.restype = ctypes.c_char_p
|
|
133
|
+
|
|
134
|
+
# Shadow / branch (git-merge) API
|
|
135
|
+
lib.tmjlite_shadow_open.argtypes = [ctypes.c_char_p]
|
|
136
|
+
lib.tmjlite_shadow_open.restype = ctypes.c_void_p
|
|
137
|
+
lib.tmjlite_shadow_open_branch.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
|
138
|
+
lib.tmjlite_shadow_open_branch.restype = ctypes.c_void_p
|
|
139
|
+
lib.tmjlite_shadow_close.argtypes = [ctypes.c_void_p]
|
|
140
|
+
lib.tmjlite_shadow_close.restype = None
|
|
141
|
+
lib.tmjlite_shadow_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
142
|
+
lib.tmjlite_shadow_exec.restype = ctypes.c_int
|
|
143
|
+
lib.tmjlite_shadow_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
144
|
+
lib.tmjlite_shadow_query.restype = ctypes.c_void_p
|
|
145
|
+
lib.tmjlite_shadow_commit.argtypes = [ctypes.c_void_p]
|
|
146
|
+
lib.tmjlite_shadow_commit.restype = ctypes.c_longlong
|
|
147
|
+
lib.tmjlite_shadow_refresh.argtypes = [ctypes.c_void_p]
|
|
148
|
+
lib.tmjlite_shadow_refresh.restype = ctypes.c_int
|
|
149
|
+
lib.tmjlite_shadow_pending.argtypes = [ctypes.c_void_p]
|
|
150
|
+
lib.tmjlite_shadow_pending.restype = ctypes.c_longlong
|
|
151
|
+
lib.tmjlite_shadow_seal.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
|
|
152
|
+
lib.tmjlite_shadow_seal.restype = ctypes.c_longlong
|
|
153
|
+
lib.tmjlite_shadow_unsealed.argtypes = [ctypes.c_void_p]
|
|
154
|
+
lib.tmjlite_shadow_unsealed.restype = ctypes.c_longlong
|
|
155
|
+
lib.tmjlite_shadow_set_merge_policy.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
|
|
156
|
+
lib.tmjlite_shadow_set_merge_policy.restype = ctypes.c_int
|
|
157
|
+
lib.tmjlite_shadow_conflicts.argtypes = [ctypes.c_void_p]
|
|
158
|
+
lib.tmjlite_shadow_conflicts.restype = ctypes.c_longlong
|
|
159
|
+
lib.tmjlite_shadow_errmsg.argtypes = [ctypes.c_void_p]
|
|
160
|
+
lib.tmjlite_shadow_errmsg.restype = ctypes.c_char_p
|
|
161
|
+
|
|
162
|
+
return lib
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_lib = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _get_lib():
|
|
169
|
+
global _lib
|
|
170
|
+
if _lib is None:
|
|
171
|
+
_lib = _load_lib()
|
|
172
|
+
return _lib
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TMJLiteError(Exception):
|
|
176
|
+
"""Error from the TMJLite engine."""
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Row:
|
|
181
|
+
"""A single row from a query result."""
|
|
182
|
+
|
|
183
|
+
__slots__ = ("_columns", "_values")
|
|
184
|
+
|
|
185
|
+
def __init__(self, columns, values):
|
|
186
|
+
self._columns = columns
|
|
187
|
+
self._values = values
|
|
188
|
+
|
|
189
|
+
def __getitem__(self, key):
|
|
190
|
+
if isinstance(key, int):
|
|
191
|
+
return self._values[key]
|
|
192
|
+
if isinstance(key, str):
|
|
193
|
+
try:
|
|
194
|
+
idx = self._columns.index(key)
|
|
195
|
+
except ValueError:
|
|
196
|
+
raise KeyError(f"column '{key}' not found")
|
|
197
|
+
return self._values[idx]
|
|
198
|
+
raise TypeError(f"indices must be int or str, not {type(key).__name__}")
|
|
199
|
+
|
|
200
|
+
def __len__(self):
|
|
201
|
+
return len(self._values)
|
|
202
|
+
|
|
203
|
+
def __repr__(self):
|
|
204
|
+
pairs = ", ".join(f"{c}={v!r}" for c, v in zip(self._columns, self._values))
|
|
205
|
+
return f"Row({pairs})"
|
|
206
|
+
|
|
207
|
+
def keys(self):
|
|
208
|
+
return list(self._columns)
|
|
209
|
+
|
|
210
|
+
def values(self):
|
|
211
|
+
return list(self._values)
|
|
212
|
+
|
|
213
|
+
def items(self):
|
|
214
|
+
return list(zip(self._columns, self._values))
|
|
215
|
+
|
|
216
|
+
def to_dict(self):
|
|
217
|
+
return dict(zip(self._columns, self._values))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class QueryResult:
|
|
221
|
+
"""Result of a SELECT query."""
|
|
222
|
+
|
|
223
|
+
__slots__ = ("columns", "rows")
|
|
224
|
+
|
|
225
|
+
def __init__(self, columns, rows):
|
|
226
|
+
self.columns = columns
|
|
227
|
+
self.rows = rows
|
|
228
|
+
|
|
229
|
+
def __len__(self):
|
|
230
|
+
return len(self.rows)
|
|
231
|
+
|
|
232
|
+
def __iter__(self):
|
|
233
|
+
return iter(self.rows)
|
|
234
|
+
|
|
235
|
+
def __getitem__(self, index):
|
|
236
|
+
return self.rows[index]
|
|
237
|
+
|
|
238
|
+
def __bool__(self):
|
|
239
|
+
return len(self.rows) > 0
|
|
240
|
+
|
|
241
|
+
def __repr__(self):
|
|
242
|
+
return f"QueryResult(columns={self.columns}, rows={len(self.rows)})"
|
|
243
|
+
|
|
244
|
+
def to_dicts(self):
|
|
245
|
+
return [row.to_dict() for row in self.rows]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _parse_value(raw_str):
|
|
249
|
+
"""Convert a string value from the C API to a Python type."""
|
|
250
|
+
if raw_str is None or raw_str == "NULL":
|
|
251
|
+
return None
|
|
252
|
+
if raw_str == "true":
|
|
253
|
+
return True
|
|
254
|
+
if raw_str == "false":
|
|
255
|
+
return False
|
|
256
|
+
try:
|
|
257
|
+
return int(raw_str)
|
|
258
|
+
except ValueError:
|
|
259
|
+
pass
|
|
260
|
+
try:
|
|
261
|
+
return float(raw_str)
|
|
262
|
+
except ValueError:
|
|
263
|
+
pass
|
|
264
|
+
return raw_str
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Connection:
|
|
268
|
+
"""Connection to a TMJLite database."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, handle):
|
|
271
|
+
self._lib = _get_lib()
|
|
272
|
+
self._handle = handle
|
|
273
|
+
|
|
274
|
+
def execute(self, sql):
|
|
275
|
+
"""Execute a non-query SQL statement. Returns rows affected info as a string."""
|
|
276
|
+
rc = self._lib.tmjlite_exec(self._handle, sql.encode("utf-8"))
|
|
277
|
+
if rc != 0:
|
|
278
|
+
err = self._lib.tmjlite_errmsg(self._handle)
|
|
279
|
+
msg = err.decode("utf-8") if err else "unknown error"
|
|
280
|
+
raise TMJLiteError(msg)
|
|
281
|
+
|
|
282
|
+
def query(self, sql):
|
|
283
|
+
"""Execute a SELECT and return a QueryResult."""
|
|
284
|
+
result_ptr = self._lib.tmjlite_query(self._handle, sql.encode("utf-8"))
|
|
285
|
+
if not result_ptr:
|
|
286
|
+
err = self._lib.tmjlite_errmsg(self._handle)
|
|
287
|
+
msg = err.decode("utf-8") if err else "unknown error"
|
|
288
|
+
raise TMJLiteError(msg)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
col_count = self._lib.tmjlite_result_column_count(result_ptr)
|
|
292
|
+
row_count = self._lib.tmjlite_result_row_count(result_ptr)
|
|
293
|
+
|
|
294
|
+
columns = []
|
|
295
|
+
for i in range(col_count):
|
|
296
|
+
name = self._lib.tmjlite_result_column_name(result_ptr, i)
|
|
297
|
+
columns.append(name.decode("utf-8") if name else "")
|
|
298
|
+
|
|
299
|
+
rows = []
|
|
300
|
+
for r in range(row_count):
|
|
301
|
+
values = []
|
|
302
|
+
for c in range(col_count):
|
|
303
|
+
raw = self._lib.tmjlite_result_value(result_ptr, r, c)
|
|
304
|
+
val_str = raw.decode("utf-8") if raw else None
|
|
305
|
+
values.append(_parse_value(val_str))
|
|
306
|
+
rows.append(Row(columns, values))
|
|
307
|
+
|
|
308
|
+
return QueryResult(columns, rows)
|
|
309
|
+
finally:
|
|
310
|
+
self._lib.tmjlite_result_free(result_ptr)
|
|
311
|
+
|
|
312
|
+
def begin(self):
|
|
313
|
+
"""Start a transaction."""
|
|
314
|
+
self.execute("BEGIN")
|
|
315
|
+
|
|
316
|
+
def commit(self):
|
|
317
|
+
"""Commit the current transaction."""
|
|
318
|
+
self.execute("COMMIT")
|
|
319
|
+
|
|
320
|
+
def rollback(self):
|
|
321
|
+
"""Roll back the current transaction."""
|
|
322
|
+
self.execute("ROLLBACK")
|
|
323
|
+
|
|
324
|
+
@contextmanager
|
|
325
|
+
def transaction(self):
|
|
326
|
+
"""Context manager: auto-commits on success, rolls back on exception."""
|
|
327
|
+
self.begin()
|
|
328
|
+
try:
|
|
329
|
+
yield
|
|
330
|
+
self.commit()
|
|
331
|
+
except Exception:
|
|
332
|
+
self.rollback()
|
|
333
|
+
raise
|
|
334
|
+
|
|
335
|
+
def save(self):
|
|
336
|
+
"""Explicitly save to the database file."""
|
|
337
|
+
rc = self._lib.tmjlite_save(self._handle)
|
|
338
|
+
if rc != 0:
|
|
339
|
+
err = self._lib.tmjlite_errmsg(self._handle)
|
|
340
|
+
msg = err.decode("utf-8") if err else "unknown error"
|
|
341
|
+
raise TMJLiteError(msg)
|
|
342
|
+
|
|
343
|
+
def save_as(self, path):
|
|
344
|
+
"""Save to a new file path."""
|
|
345
|
+
rc = self._lib.tmjlite_save_as(self._handle, path.encode("utf-8"))
|
|
346
|
+
if rc != 0:
|
|
347
|
+
err = self._lib.tmjlite_errmsg(self._handle)
|
|
348
|
+
msg = err.decode("utf-8") if err else "unknown error"
|
|
349
|
+
raise TMJLiteError(msg)
|
|
350
|
+
|
|
351
|
+
def close(self):
|
|
352
|
+
"""Close the database connection."""
|
|
353
|
+
if self._handle:
|
|
354
|
+
self._lib.tmjlite_close(self._handle)
|
|
355
|
+
self._handle = None
|
|
356
|
+
|
|
357
|
+
def __enter__(self):
|
|
358
|
+
return self
|
|
359
|
+
|
|
360
|
+
def __exit__(self, *args):
|
|
361
|
+
self.close()
|
|
362
|
+
|
|
363
|
+
def __del__(self):
|
|
364
|
+
self.close()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _read_query_result(lib, result_ptr):
|
|
368
|
+
"""Materializa um TmjResult* em QueryResult e libera o ponteiro."""
|
|
369
|
+
try:
|
|
370
|
+
col_count = lib.tmjlite_result_column_count(result_ptr)
|
|
371
|
+
row_count = lib.tmjlite_result_row_count(result_ptr)
|
|
372
|
+
columns = []
|
|
373
|
+
for i in range(col_count):
|
|
374
|
+
name = lib.tmjlite_result_column_name(result_ptr, i)
|
|
375
|
+
columns.append(name.decode("utf-8") if name else "")
|
|
376
|
+
rows = []
|
|
377
|
+
for r in range(row_count):
|
|
378
|
+
values = []
|
|
379
|
+
for c in range(col_count):
|
|
380
|
+
raw = lib.tmjlite_result_value(result_ptr, r, c)
|
|
381
|
+
val_str = raw.decode("utf-8") if raw else None
|
|
382
|
+
values.append(_parse_value(val_str))
|
|
383
|
+
rows.append(Row(columns, values))
|
|
384
|
+
return QueryResult(columns, rows)
|
|
385
|
+
finally:
|
|
386
|
+
lib.tmjlite_result_free(result_ptr)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class SharedConnection:
|
|
390
|
+
"""
|
|
391
|
+
Conexão thread-safe (MVCC): um único handle pode ser usado por N threads —
|
|
392
|
+
SELECTs rodam em snapshot em paralelo entre si e com o escritor; escritas
|
|
393
|
+
serializam e são duráveis ao retornar. Cada statement é auto-commit
|
|
394
|
+
(BEGIN/COMMIT não são suportados aqui; use Connection para transações).
|
|
395
|
+
|
|
396
|
+
Multi-processo continua exclusivo: abrir o mesmo arquivo em outro processo
|
|
397
|
+
falha com "database is locked by another process".
|
|
398
|
+
|
|
399
|
+
db = tmjlite.connect_shared("app.tmjp")
|
|
400
|
+
with ThreadPoolExecutor(8) as pool:
|
|
401
|
+
pool.map(lambda a: db.execute(...), agencies)
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
def __init__(self, handle):
|
|
405
|
+
self._lib = _get_lib()
|
|
406
|
+
self._handle = handle
|
|
407
|
+
|
|
408
|
+
def _raise_last(self):
|
|
409
|
+
err = self._lib.tmjlite_errmsg_shared(self._handle)
|
|
410
|
+
raise TMJLiteError(err.decode("utf-8") if err else "unknown error")
|
|
411
|
+
|
|
412
|
+
def execute(self, sql):
|
|
413
|
+
rc = self._lib.tmjlite_exec_shared(self._handle, sql.encode("utf-8"))
|
|
414
|
+
if rc != 0:
|
|
415
|
+
self._raise_last()
|
|
416
|
+
|
|
417
|
+
def query(self, sql):
|
|
418
|
+
result_ptr = self._lib.tmjlite_query_shared(self._handle, sql.encode("utf-8"))
|
|
419
|
+
if not result_ptr:
|
|
420
|
+
self._raise_last()
|
|
421
|
+
return _read_query_result(self._lib, result_ptr)
|
|
422
|
+
|
|
423
|
+
def close(self):
|
|
424
|
+
if self._handle:
|
|
425
|
+
self._lib.tmjlite_close_shared(self._handle)
|
|
426
|
+
self._handle = None
|
|
427
|
+
|
|
428
|
+
def __enter__(self):
|
|
429
|
+
return self
|
|
430
|
+
|
|
431
|
+
def __exit__(self, *args):
|
|
432
|
+
self.close()
|
|
433
|
+
|
|
434
|
+
def __del__(self):
|
|
435
|
+
self.close()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def connect_shared(path):
|
|
439
|
+
"""Abre uma SharedConnection (thread-safe) para um arquivo .tmjp."""
|
|
440
|
+
lib = _get_lib()
|
|
441
|
+
handle = lib.tmjlite_open_shared(path.encode("utf-8"))
|
|
442
|
+
if not handle:
|
|
443
|
+
raise TMJLiteError(
|
|
444
|
+
f"failed to open database: {path} (em uso por outro processo?)"
|
|
445
|
+
)
|
|
446
|
+
return SharedConnection(handle)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class ShadowConnection:
|
|
450
|
+
"""
|
|
451
|
+
Sessão shadow (git-merge): SQL normal sobre um snapshot do banco + as
|
|
452
|
+
próprias escritas; cada INSERT/UPDATE/DELETE vira uma op num `.tmjv` ao
|
|
453
|
+
lado do arquivo, e `commit()` faz o merge por chave primária no banco
|
|
454
|
+
principal (lock de milissegundos). N processos coexistem no mesmo arquivo
|
|
455
|
+
sem se bloquear — o que os vizinhos gravaram aparece após o merge deles
|
|
456
|
+
e um `refresh()`. DML-only: DDL e BEGIN/COMMIT são rejeitados.
|
|
457
|
+
|
|
458
|
+
Aberta com `open_branch(path, nome)` vira uma **branch nomeada** e
|
|
459
|
+
persistente (`.tmjb`): `seal()` grava um commit (autor/mensagem); só ops
|
|
460
|
+
seladas entram no `commit()`, que consome a branch.
|
|
461
|
+
|
|
462
|
+
with tmjlite.connect_shadow("app.tmjp") as sess:
|
|
463
|
+
sess.execute("INSERT INTO listing (agency_id, external_id) VALUES ('ag', 'e1');")
|
|
464
|
+
applied = sess.commit() # merge no main
|
|
465
|
+
|
|
466
|
+
br = tmjlite.open_branch("app.tmjp", "import-2026-09")
|
|
467
|
+
br.execute("UPDATE listing SET seen = 1 WHERE external_id = 'e1';")
|
|
468
|
+
br.seal("primeira leva", author="thiago")
|
|
469
|
+
br.commit()
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
def __init__(self, handle):
|
|
473
|
+
self._lib = _get_lib()
|
|
474
|
+
self._handle = handle
|
|
475
|
+
|
|
476
|
+
def _raise_last(self):
|
|
477
|
+
err = self._lib.tmjlite_shadow_errmsg(self._handle)
|
|
478
|
+
raise TMJLiteError(err.decode("utf-8") if err else "unknown error")
|
|
479
|
+
|
|
480
|
+
def execute(self, sql):
|
|
481
|
+
"""INSERT/UPDATE/DELETE viram ops da sessão; SELECT roda no overlay."""
|
|
482
|
+
if self._lib.tmjlite_shadow_exec(self._handle, sql.encode("utf-8")) != 0:
|
|
483
|
+
self._raise_last()
|
|
484
|
+
|
|
485
|
+
def query(self, sql):
|
|
486
|
+
"""SELECT sobre snapshot + próprias escritas."""
|
|
487
|
+
result_ptr = self._lib.tmjlite_shadow_query(self._handle, sql.encode("utf-8"))
|
|
488
|
+
if not result_ptr:
|
|
489
|
+
self._raise_last()
|
|
490
|
+
return _read_query_result(self._lib, result_ptr)
|
|
491
|
+
|
|
492
|
+
def commit(self):
|
|
493
|
+
"""Merge no banco principal. Retorna o nº de ops aplicadas. Numa
|
|
494
|
+
branch, só ops seladas; ops soltas depois do último `seal` → erro."""
|
|
495
|
+
n = self._lib.tmjlite_shadow_commit(self._handle)
|
|
496
|
+
if n < 0:
|
|
497
|
+
self._raise_last()
|
|
498
|
+
return n
|
|
499
|
+
|
|
500
|
+
def refresh(self):
|
|
501
|
+
"""Re-snapshota o main (vê merges dos vizinhos) mantendo as próprias ops."""
|
|
502
|
+
if self._lib.tmjlite_shadow_refresh(self._handle) != 0:
|
|
503
|
+
self._raise_last()
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def pending_ops(self):
|
|
507
|
+
"""Ops registradas e ainda não mergeadas."""
|
|
508
|
+
return self._lib.tmjlite_shadow_pending(self._handle)
|
|
509
|
+
|
|
510
|
+
def seal(self, message, author=None):
|
|
511
|
+
"""Branch nomeada: sela as ops não commitadas num commit. Retorna o nº de ops."""
|
|
512
|
+
author = author or os.environ.get("TMJLITE_AUTHOR") or os.environ.get("USER") or "python"
|
|
513
|
+
n = self._lib.tmjlite_shadow_seal(
|
|
514
|
+
self._handle, author.encode("utf-8"), (message or "").encode("utf-8")
|
|
515
|
+
)
|
|
516
|
+
if n < 0:
|
|
517
|
+
self._raise_last()
|
|
518
|
+
return n
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def unsealed_ops(self):
|
|
522
|
+
"""Ops registradas depois do último `seal` (0 em sessão anônima)."""
|
|
523
|
+
return self._lib.tmjlite_shadow_unsealed(self._handle)
|
|
524
|
+
|
|
525
|
+
def set_merge_policy(self, policy, table=None):
|
|
526
|
+
"""Política de conflito do commit: 'lww' | 'error' | 'field_merge' | 'manual'.
|
|
527
|
+
`table=None` define o default da sessão; senão só aquela tabela."""
|
|
528
|
+
t = table.encode("utf-8") if table else None
|
|
529
|
+
if self._lib.tmjlite_shadow_set_merge_policy(self._handle, t, policy.encode("utf-8")) != 0:
|
|
530
|
+
self._raise_last()
|
|
531
|
+
|
|
532
|
+
def conflicts(self):
|
|
533
|
+
"""Dry-run do commit: descrição dos conflitos que o merge encontraria."""
|
|
534
|
+
n = self._lib.tmjlite_shadow_conflicts(self._handle)
|
|
535
|
+
if n < 0:
|
|
536
|
+
self._raise_last()
|
|
537
|
+
if n == 0:
|
|
538
|
+
return []
|
|
539
|
+
err = self._lib.tmjlite_shadow_errmsg(self._handle)
|
|
540
|
+
return err.decode("utf-8").split("\n") if err else []
|
|
541
|
+
|
|
542
|
+
def close(self):
|
|
543
|
+
"""Fecha a sessão. Ops não mergeadas ficam no disco (mergeáveis depois)."""
|
|
544
|
+
if self._handle:
|
|
545
|
+
self._lib.tmjlite_shadow_close(self._handle)
|
|
546
|
+
self._handle = None
|
|
547
|
+
|
|
548
|
+
def __enter__(self):
|
|
549
|
+
return self
|
|
550
|
+
|
|
551
|
+
def __exit__(self, *args):
|
|
552
|
+
self.close()
|
|
553
|
+
|
|
554
|
+
def __del__(self):
|
|
555
|
+
self.close()
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def connect_shadow(path):
|
|
559
|
+
"""Abre uma sessão shadow anônima (escrita multi-processo por merge)."""
|
|
560
|
+
lib = _get_lib()
|
|
561
|
+
handle = lib.tmjlite_shadow_open(path.encode("utf-8"))
|
|
562
|
+
if not handle:
|
|
563
|
+
raise TMJLiteError(f"failed to open shadow session on: {path}")
|
|
564
|
+
return ShadowConnection(handle)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def open_branch(path, name):
|
|
568
|
+
"""Abre (ou cria) a branch nomeada `name` em `<path>.shadows/<name>.tmjb`."""
|
|
569
|
+
lib = _get_lib()
|
|
570
|
+
handle = lib.tmjlite_shadow_open_branch(path.encode("utf-8"), name.encode("utf-8"))
|
|
571
|
+
if not handle:
|
|
572
|
+
raise TMJLiteError(f"failed to open branch '{name}' on: {path}")
|
|
573
|
+
return ShadowConnection(handle)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def connect(path=":memory:"):
|
|
577
|
+
"""
|
|
578
|
+
Open a TMJLite database connection.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
path: File path, or ":memory:" for an in-memory database.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Connection object.
|
|
585
|
+
"""
|
|
586
|
+
lib = _get_lib()
|
|
587
|
+
if path == ":memory:":
|
|
588
|
+
handle = lib.tmjlite_open_memory()
|
|
589
|
+
else:
|
|
590
|
+
handle = lib.tmjlite_open(path.encode("utf-8"))
|
|
591
|
+
|
|
592
|
+
if not handle:
|
|
593
|
+
raise TMJLiteError(f"failed to open database: {path}")
|
|
594
|
+
|
|
595
|
+
return Connection(handle)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tmjlite
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: TMJLite embedded SQL database driver — git-native branches, single-file .tmjp, native engine included
|
|
5
|
+
Home-page: https://tmjlite.com.br
|
|
6
|
+
Author: TMJ Sistemas de Informação
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://tmjlite.com.br
|
|
9
|
+
Project-URL: Documentation, https://tmjlite.com.br/manual.html
|
|
10
|
+
Project-URL: Repository, https://github.com/TMJacometti/TMJPlatform
|
|
11
|
+
Project-URL: Changelog, https://github.com/TMJacometti/TMJPlatform/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: database,embedded,sql,tmjlite,branches
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Rust
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
20
|
+
Classifier: Operating System :: MacOS
|
|
21
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
22
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
|
|
28
|
+
# tmjlite (Python)
|
|
29
|
+
|
|
30
|
+
Driver Python do **TMJLite** — banco de dados SQL embarcado, em um único
|
|
31
|
+
arquivo `.tmjp`, com **branches de dados git-native**. O motor nativo (Rust)
|
|
32
|
+
já vem dentro do wheel: nada para instalar além do `pip`.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install tmjlite
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Plataformas: macOS (Apple Silicon), Linux x86-64, Windows x86-64 · Python ≥ 3.8.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import tmjlite
|
|
42
|
+
|
|
43
|
+
db = tmjlite.connect("app.tmjp")
|
|
44
|
+
db.execute("CREATE TABLE users (id PK, name TEXT, age INT);")
|
|
45
|
+
db.execute("INSERT INTO users (name, age) VALUES ('Alice', 30);")
|
|
46
|
+
for row in db.query("SELECT name, age FROM users WHERE age > 18;"):
|
|
47
|
+
print(row["name"], row["age"])
|
|
48
|
+
|
|
49
|
+
with db.transaction():
|
|
50
|
+
db.execute("INSERT INTO users (name, age) VALUES ('Bob', 25);")
|
|
51
|
+
db.close()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Branches — trabalhe numa cópia lógica e faça merge no arquivo principal,
|
|
55
|
+
enquanto a aplicação continua enxergando o banco intocado:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
b = tmjlite.open_branch("app.tmjp", "importacao")
|
|
59
|
+
b.execute("INSERT INTO users (name, age) VALUES ('Carol', 41);")
|
|
60
|
+
b.seal("primeira leva", author="thiago") # commit na branch
|
|
61
|
+
applied = b.commit() # merge no app.tmjp
|
|
62
|
+
print(f"{applied} linha(s) aplicadas")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Vários processos ao mesmo tempo? `tmjlite.connect_shadow(path)` dá a cada
|
|
66
|
+
worker uma sessão própria; `commit()` mergeia por chave primária.
|
|
67
|
+
|
|
68
|
+
- Manual: <https://tmjlite.com.br/manual.html>
|
|
69
|
+
- Branches e concorrência (guia + FAQ): <https://tmjlite.com.br/branches.html>
|
|
70
|
+
- Exemplo completo (sistema de cadastro): <https://tmjlite.com.br/examples.html>
|
|
71
|
+
- Changelog: <https://github.com/TMJacometti/TMJPlatform/blob/main/CHANGELOG.md>
|
|
72
|
+
|
|
73
|
+
Licença MIT · TMJ Sistemas de Informação.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tmjlite/__init__.py,sha256=YWXB2_j2AV3pnykwDadBgylZ8qd1h905IHXEt-NUsqY,817
|
|
2
|
+
tmjlite/driver.py,sha256=dNZ0HCZb1IY7Iyr-0SobkDpCB0qyNxPDaX5LtMiC-_8,19636
|
|
3
|
+
tmjlite/_lib/win32-x64/tmjlite_ffi.dll,sha256=jXB3FJR17biXfpeMTqaT08lZ_aRviViNlbfdhOvHS0k,1639936
|
|
4
|
+
tmjlite-0.2.8.dist-info/METADATA,sha256=0bVNsAcvMZ8OpUatATkS8bUezW10v-N3lezKeDkBYDg,2792
|
|
5
|
+
tmjlite-0.2.8.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
|
|
6
|
+
tmjlite-0.2.8.dist-info/top_level.txt,sha256=XZC3Qcm1WAGoYeD6HoglS68FZFLPpPfqhb-m7kxsfWk,8
|
|
7
|
+
tmjlite-0.2.8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tmjlite
|