jasonisnthappy 0.1.0__py3-none-manylinux2014_x86_64.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.
- jasonisnthappy/__init__.py +8 -0
- jasonisnthappy/database.py +1901 -0
- jasonisnthappy/libjasonisnthappy.so +0 -0
- jasonisnthappy/loader.py +63 -0
- jasonisnthappy-0.1.0.dist-info/METADATA +10 -0
- jasonisnthappy-0.1.0.dist-info/RECORD +8 -0
- jasonisnthappy-0.1.0.dist-info/WHEEL +5 -0
- jasonisnthappy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python bindings for jasonisnthappy database using ctypes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import json
|
|
7
|
+
import platform
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class UpsertResult:
|
|
15
|
+
"""Result of an upsert operation."""
|
|
16
|
+
id: str
|
|
17
|
+
inserted: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_library_path():
|
|
21
|
+
"""Get library path, checking package directory first, then fallback to loader."""
|
|
22
|
+
# First check if library exists in package directory (installed during pip install)
|
|
23
|
+
system = platform.system()
|
|
24
|
+
machine = platform.machine().lower()
|
|
25
|
+
|
|
26
|
+
if system == "Darwin":
|
|
27
|
+
lib_dir = "darwin-arm64" if machine == "arm64" else "darwin-amd64"
|
|
28
|
+
lib_name = "libjasonisnthappy.dylib"
|
|
29
|
+
elif system == "Linux":
|
|
30
|
+
lib_dir = "linux-arm64" if machine in ("aarch64", "arm64") else "linux-amd64"
|
|
31
|
+
lib_name = "libjasonisnthappy.so"
|
|
32
|
+
elif system == "Windows":
|
|
33
|
+
lib_dir = "windows-amd64"
|
|
34
|
+
lib_name = "jasonisnthappy.dll"
|
|
35
|
+
else:
|
|
36
|
+
# Unsupported platform, let loader handle it
|
|
37
|
+
from .loader import get_library_path
|
|
38
|
+
return get_library_path()
|
|
39
|
+
|
|
40
|
+
# Check package lib directory first
|
|
41
|
+
package_lib_path = Path(__file__).parent / "lib" / lib_dir / lib_name
|
|
42
|
+
if package_lib_path.exists():
|
|
43
|
+
return str(package_lib_path)
|
|
44
|
+
|
|
45
|
+
# Fallback to loader (downloads to ~/.jasonisnthappy/)
|
|
46
|
+
from .loader import get_library_path
|
|
47
|
+
return get_library_path()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Load the library (from package dir or auto-download if needed)
|
|
51
|
+
_lib = ctypes.CDLL(_get_library_path())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ==================
|
|
55
|
+
# C Structures
|
|
56
|
+
# ==================
|
|
57
|
+
|
|
58
|
+
class CError(ctypes.Structure):
|
|
59
|
+
_fields_ = [
|
|
60
|
+
("code", ctypes.c_int32),
|
|
61
|
+
("message", ctypes.c_char_p),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CDatabaseOptions(ctypes.Structure):
|
|
66
|
+
_fields_ = [
|
|
67
|
+
("cache_size", ctypes.c_size_t),
|
|
68
|
+
("auto_checkpoint_threshold", ctypes.c_uint64),
|
|
69
|
+
("file_permissions", ctypes.c_uint32),
|
|
70
|
+
("read_only", ctypes.c_bool),
|
|
71
|
+
("max_bulk_operations", ctypes.c_size_t),
|
|
72
|
+
("max_document_size", ctypes.c_size_t),
|
|
73
|
+
("max_request_body_size", ctypes.c_size_t),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CTransactionConfig(ctypes.Structure):
|
|
78
|
+
_fields_ = [
|
|
79
|
+
("max_retries", ctypes.c_size_t),
|
|
80
|
+
("retry_backoff_base_ms", ctypes.c_uint64),
|
|
81
|
+
("max_retry_backoff_ms", ctypes.c_uint64),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ==================
|
|
86
|
+
# Function Signatures (85 total)
|
|
87
|
+
# ==================
|
|
88
|
+
|
|
89
|
+
# Database Operations (21)
|
|
90
|
+
_lib.jasonisnthappy_open.argtypes = [ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
91
|
+
_lib.jasonisnthappy_open.restype = ctypes.c_void_p
|
|
92
|
+
|
|
93
|
+
_lib.jasonisnthappy_open_with_options.argtypes = [ctypes.c_char_p, CDatabaseOptions, ctypes.POINTER(CError)]
|
|
94
|
+
_lib.jasonisnthappy_open_with_options.restype = ctypes.c_void_p
|
|
95
|
+
|
|
96
|
+
_lib.jasonisnthappy_close.argtypes = [ctypes.c_void_p]
|
|
97
|
+
_lib.jasonisnthappy_close.restype = None
|
|
98
|
+
|
|
99
|
+
_lib.jasonisnthappy_default_database_options.argtypes = []
|
|
100
|
+
_lib.jasonisnthappy_default_database_options.restype = CDatabaseOptions
|
|
101
|
+
|
|
102
|
+
_lib.jasonisnthappy_default_transaction_config.argtypes = []
|
|
103
|
+
_lib.jasonisnthappy_default_transaction_config.restype = CTransactionConfig
|
|
104
|
+
|
|
105
|
+
_lib.jasonisnthappy_set_transaction_config.argtypes = [ctypes.c_void_p, CTransactionConfig, ctypes.POINTER(CError)]
|
|
106
|
+
_lib.jasonisnthappy_set_transaction_config.restype = ctypes.c_int32
|
|
107
|
+
|
|
108
|
+
_lib.jasonisnthappy_get_transaction_config.argtypes = [ctypes.c_void_p, ctypes.POINTER(CTransactionConfig), ctypes.POINTER(CError)]
|
|
109
|
+
_lib.jasonisnthappy_get_transaction_config.restype = ctypes.c_int32
|
|
110
|
+
|
|
111
|
+
_lib.jasonisnthappy_set_auto_checkpoint_threshold.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(CError)]
|
|
112
|
+
_lib.jasonisnthappy_set_auto_checkpoint_threshold.restype = ctypes.c_int32
|
|
113
|
+
|
|
114
|
+
_lib.jasonisnthappy_get_path.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
115
|
+
_lib.jasonisnthappy_get_path.restype = ctypes.c_int32
|
|
116
|
+
|
|
117
|
+
_lib.jasonisnthappy_is_read_only.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_bool), ctypes.POINTER(CError)]
|
|
118
|
+
_lib.jasonisnthappy_is_read_only.restype = ctypes.c_int32
|
|
119
|
+
|
|
120
|
+
_lib.jasonisnthappy_max_bulk_operations.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
121
|
+
_lib.jasonisnthappy_max_bulk_operations.restype = ctypes.c_size_t
|
|
122
|
+
|
|
123
|
+
_lib.jasonisnthappy_max_document_size.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
124
|
+
_lib.jasonisnthappy_max_document_size.restype = ctypes.c_size_t
|
|
125
|
+
|
|
126
|
+
_lib.jasonisnthappy_max_request_body_size.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
127
|
+
_lib.jasonisnthappy_max_request_body_size.restype = ctypes.c_size_t
|
|
128
|
+
|
|
129
|
+
_lib.jasonisnthappy_list_collections.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
130
|
+
_lib.jasonisnthappy_list_collections.restype = ctypes.c_int32
|
|
131
|
+
|
|
132
|
+
_lib.jasonisnthappy_collection_stats.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
133
|
+
_lib.jasonisnthappy_collection_stats.restype = ctypes.c_int32
|
|
134
|
+
|
|
135
|
+
_lib.jasonisnthappy_database_info.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
136
|
+
_lib.jasonisnthappy_database_info.restype = ctypes.c_int32
|
|
137
|
+
|
|
138
|
+
_lib.jasonisnthappy_list_indexes.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
139
|
+
_lib.jasonisnthappy_list_indexes.restype = ctypes.c_int32
|
|
140
|
+
|
|
141
|
+
_lib.jasonisnthappy_create_index.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool, ctypes.POINTER(CError)]
|
|
142
|
+
_lib.jasonisnthappy_create_index.restype = ctypes.c_int32
|
|
143
|
+
|
|
144
|
+
_lib.jasonisnthappy_create_compound_index.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool, ctypes.POINTER(CError)]
|
|
145
|
+
_lib.jasonisnthappy_create_compound_index.restype = ctypes.c_int32
|
|
146
|
+
|
|
147
|
+
_lib.jasonisnthappy_create_text_index.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
148
|
+
_lib.jasonisnthappy_create_text_index.restype = ctypes.c_int32
|
|
149
|
+
|
|
150
|
+
_lib.jasonisnthappy_drop_index.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
151
|
+
_lib.jasonisnthappy_drop_index.restype = ctypes.c_int32
|
|
152
|
+
|
|
153
|
+
_lib.jasonisnthappy_set_schema.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
154
|
+
_lib.jasonisnthappy_set_schema.restype = ctypes.c_int32
|
|
155
|
+
|
|
156
|
+
_lib.jasonisnthappy_get_schema.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
157
|
+
_lib.jasonisnthappy_get_schema.restype = ctypes.c_int32
|
|
158
|
+
|
|
159
|
+
_lib.jasonisnthappy_remove_schema.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
160
|
+
_lib.jasonisnthappy_remove_schema.restype = ctypes.c_int32
|
|
161
|
+
|
|
162
|
+
# Maintenance & Monitoring (6)
|
|
163
|
+
_lib.jasonisnthappy_checkpoint.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
164
|
+
_lib.jasonisnthappy_checkpoint.restype = ctypes.c_int32
|
|
165
|
+
|
|
166
|
+
_lib.jasonisnthappy_backup.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
167
|
+
_lib.jasonisnthappy_backup.restype = ctypes.c_int32
|
|
168
|
+
|
|
169
|
+
_lib.jasonisnthappy_verify_backup.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
170
|
+
_lib.jasonisnthappy_verify_backup.restype = ctypes.c_int32
|
|
171
|
+
|
|
172
|
+
_lib.jasonisnthappy_garbage_collect.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
173
|
+
_lib.jasonisnthappy_garbage_collect.restype = ctypes.c_int32
|
|
174
|
+
|
|
175
|
+
_lib.jasonisnthappy_metrics.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
176
|
+
_lib.jasonisnthappy_metrics.restype = ctypes.c_int32
|
|
177
|
+
|
|
178
|
+
_lib.jasonisnthappy_frame_count.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
179
|
+
_lib.jasonisnthappy_frame_count.restype = ctypes.c_int32
|
|
180
|
+
|
|
181
|
+
# Web Server
|
|
182
|
+
_lib.jasonisnthappy_start_web_server.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
183
|
+
_lib.jasonisnthappy_start_web_server.restype = ctypes.c_void_p
|
|
184
|
+
|
|
185
|
+
_lib.jasonisnthappy_stop_web_server.argtypes = [ctypes.c_void_p]
|
|
186
|
+
_lib.jasonisnthappy_stop_web_server.restype = None
|
|
187
|
+
|
|
188
|
+
# Watch callback type: void (*)(const char*, const char*, const char*, const char*, void*)
|
|
189
|
+
WatchCallbackType = ctypes.CFUNCTYPE(
|
|
190
|
+
None, # return type
|
|
191
|
+
ctypes.c_char_p, # collection
|
|
192
|
+
ctypes.c_char_p, # operation
|
|
193
|
+
ctypes.c_char_p, # doc_id
|
|
194
|
+
ctypes.c_char_p, # doc_json
|
|
195
|
+
ctypes.c_void_p, # user_data
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
_lib.jasonisnthappy_collection_watch_start.argtypes = [
|
|
199
|
+
ctypes.c_void_p, # coll
|
|
200
|
+
ctypes.c_char_p, # filter
|
|
201
|
+
WatchCallbackType, # callback
|
|
202
|
+
ctypes.c_void_p, # user_data
|
|
203
|
+
ctypes.POINTER(ctypes.c_void_p), # handle_out
|
|
204
|
+
ctypes.POINTER(CError), # error_out
|
|
205
|
+
]
|
|
206
|
+
_lib.jasonisnthappy_collection_watch_start.restype = ctypes.c_int32
|
|
207
|
+
|
|
208
|
+
_lib.jasonisnthappy_watch_stop.argtypes = [ctypes.c_void_p]
|
|
209
|
+
_lib.jasonisnthappy_watch_stop.restype = None
|
|
210
|
+
|
|
211
|
+
# Transaction Operations (14)
|
|
212
|
+
_lib.jasonisnthappy_begin_transaction.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
213
|
+
_lib.jasonisnthappy_begin_transaction.restype = ctypes.c_void_p
|
|
214
|
+
|
|
215
|
+
_lib.jasonisnthappy_commit.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
216
|
+
_lib.jasonisnthappy_commit.restype = ctypes.c_int32
|
|
217
|
+
|
|
218
|
+
_lib.jasonisnthappy_rollback.argtypes = [ctypes.c_void_p]
|
|
219
|
+
_lib.jasonisnthappy_rollback.restype = None
|
|
220
|
+
|
|
221
|
+
_lib.jasonisnthappy_transaction_is_active.argtypes = [ctypes.c_void_p, ctypes.POINTER(CError)]
|
|
222
|
+
_lib.jasonisnthappy_transaction_is_active.restype = ctypes.c_int32
|
|
223
|
+
|
|
224
|
+
_lib.jasonisnthappy_insert.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
225
|
+
_lib.jasonisnthappy_insert.restype = ctypes.c_int32
|
|
226
|
+
|
|
227
|
+
_lib.jasonisnthappy_find_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
228
|
+
_lib.jasonisnthappy_find_by_id.restype = ctypes.c_int32
|
|
229
|
+
|
|
230
|
+
_lib.jasonisnthappy_update_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
231
|
+
_lib.jasonisnthappy_update_by_id.restype = ctypes.c_int32
|
|
232
|
+
|
|
233
|
+
_lib.jasonisnthappy_delete_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
234
|
+
_lib.jasonisnthappy_delete_by_id.restype = ctypes.c_int32
|
|
235
|
+
|
|
236
|
+
_lib.jasonisnthappy_find_all.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
237
|
+
_lib.jasonisnthappy_find_all.restype = ctypes.c_int32
|
|
238
|
+
|
|
239
|
+
_lib.jasonisnthappy_count.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
240
|
+
_lib.jasonisnthappy_count.restype = ctypes.c_int32
|
|
241
|
+
|
|
242
|
+
_lib.jasonisnthappy_create_collection.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
243
|
+
_lib.jasonisnthappy_create_collection.restype = ctypes.c_int32
|
|
244
|
+
|
|
245
|
+
_lib.jasonisnthappy_drop_collection.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
246
|
+
_lib.jasonisnthappy_drop_collection.restype = ctypes.c_int32
|
|
247
|
+
|
|
248
|
+
_lib.jasonisnthappy_rename_collection.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
249
|
+
_lib.jasonisnthappy_rename_collection.restype = ctypes.c_int32
|
|
250
|
+
|
|
251
|
+
# Collection Operations (50+)
|
|
252
|
+
_lib.jasonisnthappy_get_collection.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
253
|
+
_lib.jasonisnthappy_get_collection.restype = ctypes.c_void_p
|
|
254
|
+
|
|
255
|
+
_lib.jasonisnthappy_collection_free.argtypes = [ctypes.c_void_p]
|
|
256
|
+
_lib.jasonisnthappy_collection_free.restype = None
|
|
257
|
+
|
|
258
|
+
_lib.jasonisnthappy_collection_insert.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
259
|
+
_lib.jasonisnthappy_collection_insert.restype = ctypes.c_int32
|
|
260
|
+
|
|
261
|
+
_lib.jasonisnthappy_collection_find_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
262
|
+
_lib.jasonisnthappy_collection_find_by_id.restype = ctypes.c_int32
|
|
263
|
+
|
|
264
|
+
_lib.jasonisnthappy_collection_update_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
265
|
+
_lib.jasonisnthappy_collection_update_by_id.restype = ctypes.c_int32
|
|
266
|
+
|
|
267
|
+
_lib.jasonisnthappy_collection_delete_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(CError)]
|
|
268
|
+
_lib.jasonisnthappy_collection_delete_by_id.restype = ctypes.c_int32
|
|
269
|
+
|
|
270
|
+
_lib.jasonisnthappy_collection_find_all.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
271
|
+
_lib.jasonisnthappy_collection_find_all.restype = ctypes.c_int32
|
|
272
|
+
|
|
273
|
+
_lib.jasonisnthappy_collection_count.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
274
|
+
_lib.jasonisnthappy_collection_count.restype = ctypes.c_int32
|
|
275
|
+
|
|
276
|
+
_lib.jasonisnthappy_collection_name.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
277
|
+
_lib.jasonisnthappy_collection_name.restype = ctypes.c_int32
|
|
278
|
+
|
|
279
|
+
_lib.jasonisnthappy_collection_find.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
280
|
+
_lib.jasonisnthappy_collection_find.restype = ctypes.c_int32
|
|
281
|
+
|
|
282
|
+
_lib.jasonisnthappy_collection_find_one.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
283
|
+
_lib.jasonisnthappy_collection_find_one.restype = ctypes.c_int32
|
|
284
|
+
|
|
285
|
+
_lib.jasonisnthappy_collection_update.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
286
|
+
_lib.jasonisnthappy_collection_update.restype = ctypes.c_int32
|
|
287
|
+
|
|
288
|
+
_lib.jasonisnthappy_collection_update_one.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_bool), ctypes.POINTER(CError)]
|
|
289
|
+
_lib.jasonisnthappy_collection_update_one.restype = ctypes.c_int32
|
|
290
|
+
|
|
291
|
+
_lib.jasonisnthappy_collection_delete.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
292
|
+
_lib.jasonisnthappy_collection_delete.restype = ctypes.c_int32
|
|
293
|
+
|
|
294
|
+
_lib.jasonisnthappy_collection_delete_one.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_bool), ctypes.POINTER(CError)]
|
|
295
|
+
_lib.jasonisnthappy_collection_delete_one.restype = ctypes.c_int32
|
|
296
|
+
|
|
297
|
+
_lib.jasonisnthappy_collection_upsert_by_id.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
298
|
+
_lib.jasonisnthappy_collection_upsert_by_id.restype = ctypes.c_int32
|
|
299
|
+
|
|
300
|
+
_lib.jasonisnthappy_collection_upsert.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
301
|
+
_lib.jasonisnthappy_collection_upsert.restype = ctypes.c_int32
|
|
302
|
+
|
|
303
|
+
_lib.jasonisnthappy_collection_insert_many.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
304
|
+
_lib.jasonisnthappy_collection_insert_many.restype = ctypes.c_int32
|
|
305
|
+
|
|
306
|
+
_lib.jasonisnthappy_collection_distinct.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
307
|
+
_lib.jasonisnthappy_collection_distinct.restype = ctypes.c_int32
|
|
308
|
+
|
|
309
|
+
_lib.jasonisnthappy_collection_count_distinct.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
310
|
+
_lib.jasonisnthappy_collection_count_distinct.restype = ctypes.c_int32
|
|
311
|
+
|
|
312
|
+
_lib.jasonisnthappy_collection_search.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
313
|
+
_lib.jasonisnthappy_collection_search.restype = ctypes.c_int32
|
|
314
|
+
|
|
315
|
+
_lib.jasonisnthappy_collection_count_with_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(CError)]
|
|
316
|
+
_lib.jasonisnthappy_collection_count_with_query.restype = ctypes.c_int32
|
|
317
|
+
|
|
318
|
+
_lib.jasonisnthappy_collection_query_with_options.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
319
|
+
_lib.jasonisnthappy_collection_query_with_options.restype = ctypes.c_int32
|
|
320
|
+
|
|
321
|
+
_lib.jasonisnthappy_collection_query_count.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(CError)]
|
|
322
|
+
_lib.jasonisnthappy_collection_query_count.restype = ctypes.c_int32
|
|
323
|
+
|
|
324
|
+
_lib.jasonisnthappy_collection_query_first.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
325
|
+
_lib.jasonisnthappy_collection_query_first.restype = ctypes.c_int32
|
|
326
|
+
|
|
327
|
+
_lib.jasonisnthappy_collection_bulk_write.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_bool, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
328
|
+
_lib.jasonisnthappy_collection_bulk_write.restype = ctypes.c_int32
|
|
329
|
+
|
|
330
|
+
_lib.jasonisnthappy_collection_aggregate.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(CError)]
|
|
331
|
+
_lib.jasonisnthappy_collection_aggregate.restype = ctypes.c_int32
|
|
332
|
+
|
|
333
|
+
# Utility (2)
|
|
334
|
+
_lib.jasonisnthappy_free_string.argtypes = [ctypes.c_char_p]
|
|
335
|
+
_lib.jasonisnthappy_free_string.restype = None
|
|
336
|
+
|
|
337
|
+
_lib.jasonisnthappy_free_error.argtypes = [CError]
|
|
338
|
+
_lib.jasonisnthappy_free_error.restype = None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ==================
|
|
342
|
+
# Helper Functions
|
|
343
|
+
# ==================
|
|
344
|
+
|
|
345
|
+
def _check_error(error: CError) -> None:
|
|
346
|
+
"""Check if an error occurred and raise an exception if so."""
|
|
347
|
+
if error.code != 0 and error.message:
|
|
348
|
+
message = error.message.decode("utf-8")
|
|
349
|
+
_lib.jasonisnthappy_free_error(error)
|
|
350
|
+
raise RuntimeError(message)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ==================
|
|
354
|
+
# Database Class
|
|
355
|
+
# ==================
|
|
356
|
+
|
|
357
|
+
class Database:
|
|
358
|
+
"""
|
|
359
|
+
Database represents a jasonisnthappy database instance.
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
>>> db = Database.open("./my_database.db")
|
|
363
|
+
>>> try:
|
|
364
|
+
... tx = db.begin_transaction()
|
|
365
|
+
... # ... work with transaction
|
|
366
|
+
... finally:
|
|
367
|
+
... db.close()
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
def __init__(self, db_ptr: int):
|
|
371
|
+
self._db = db_ptr
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def open(path: str) -> "Database":
|
|
375
|
+
"""Opens a database at the specified path."""
|
|
376
|
+
error = CError()
|
|
377
|
+
db_ptr = _lib.jasonisnthappy_open(path.encode("utf-8"), ctypes.byref(error))
|
|
378
|
+
|
|
379
|
+
if not db_ptr:
|
|
380
|
+
_check_error(error)
|
|
381
|
+
raise RuntimeError("Failed to open database")
|
|
382
|
+
|
|
383
|
+
return Database(db_ptr)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def open_with_options(path: str, options: CDatabaseOptions) -> "Database":
|
|
387
|
+
"""Opens a database with custom options."""
|
|
388
|
+
error = CError()
|
|
389
|
+
db_ptr = _lib.jasonisnthappy_open_with_options(path.encode("utf-8"), options, ctypes.byref(error))
|
|
390
|
+
|
|
391
|
+
if not db_ptr:
|
|
392
|
+
_check_error(error)
|
|
393
|
+
raise RuntimeError("Failed to open database")
|
|
394
|
+
|
|
395
|
+
return Database(db_ptr)
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def default_database_options() -> CDatabaseOptions:
|
|
399
|
+
"""Returns default database options."""
|
|
400
|
+
return _lib.jasonisnthappy_default_database_options()
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def default_transaction_config() -> CTransactionConfig:
|
|
404
|
+
"""Returns default transaction configuration."""
|
|
405
|
+
return _lib.jasonisnthappy_default_transaction_config()
|
|
406
|
+
|
|
407
|
+
def close(self) -> None:
|
|
408
|
+
"""Closes the database and frees associated resources."""
|
|
409
|
+
if self._db:
|
|
410
|
+
_lib.jasonisnthappy_close(self._db)
|
|
411
|
+
self._db = None
|
|
412
|
+
|
|
413
|
+
# Configuration
|
|
414
|
+
def set_transaction_config(self, config: CTransactionConfig) -> None:
|
|
415
|
+
"""Sets the transaction configuration."""
|
|
416
|
+
if not self._db:
|
|
417
|
+
raise RuntimeError("Database is closed")
|
|
418
|
+
|
|
419
|
+
error = CError()
|
|
420
|
+
result = _lib.jasonisnthappy_set_transaction_config(self._db, config, ctypes.byref(error))
|
|
421
|
+
|
|
422
|
+
if result != 0:
|
|
423
|
+
_check_error(error)
|
|
424
|
+
raise RuntimeError("Failed to set transaction config")
|
|
425
|
+
|
|
426
|
+
def get_transaction_config(self) -> CTransactionConfig:
|
|
427
|
+
"""Gets the current transaction configuration."""
|
|
428
|
+
if not self._db:
|
|
429
|
+
raise RuntimeError("Database is closed")
|
|
430
|
+
|
|
431
|
+
config = CTransactionConfig()
|
|
432
|
+
error = CError()
|
|
433
|
+
result = _lib.jasonisnthappy_get_transaction_config(self._db, ctypes.byref(config), ctypes.byref(error))
|
|
434
|
+
|
|
435
|
+
if result != 0:
|
|
436
|
+
_check_error(error)
|
|
437
|
+
raise RuntimeError("Failed to get transaction config")
|
|
438
|
+
|
|
439
|
+
return config
|
|
440
|
+
|
|
441
|
+
def set_auto_checkpoint_threshold(self, threshold: int) -> None:
|
|
442
|
+
"""Sets the auto-checkpoint threshold in WAL frames."""
|
|
443
|
+
if not self._db:
|
|
444
|
+
raise RuntimeError("Database is closed")
|
|
445
|
+
|
|
446
|
+
error = CError()
|
|
447
|
+
result = _lib.jasonisnthappy_set_auto_checkpoint_threshold(self._db, threshold, ctypes.byref(error))
|
|
448
|
+
|
|
449
|
+
if result != 0:
|
|
450
|
+
_check_error(error)
|
|
451
|
+
raise RuntimeError("Failed to set auto-checkpoint threshold")
|
|
452
|
+
|
|
453
|
+
# Database Info
|
|
454
|
+
def get_path(self) -> str:
|
|
455
|
+
"""Gets the database file path."""
|
|
456
|
+
if not self._db:
|
|
457
|
+
raise RuntimeError("Database is closed")
|
|
458
|
+
|
|
459
|
+
path_out = ctypes.c_char_p()
|
|
460
|
+
error = CError()
|
|
461
|
+
result = _lib.jasonisnthappy_get_path(self._db, ctypes.byref(path_out), ctypes.byref(error))
|
|
462
|
+
|
|
463
|
+
if result != 0:
|
|
464
|
+
_check_error(error)
|
|
465
|
+
raise RuntimeError("Failed to get path")
|
|
466
|
+
|
|
467
|
+
path = path_out.value.decode("utf-8")
|
|
468
|
+
_lib.jasonisnthappy_free_string(path_out)
|
|
469
|
+
return path
|
|
470
|
+
|
|
471
|
+
def is_read_only(self) -> bool:
|
|
472
|
+
"""Checks if the database is read-only."""
|
|
473
|
+
if not self._db:
|
|
474
|
+
raise RuntimeError("Database is closed")
|
|
475
|
+
|
|
476
|
+
read_only = ctypes.c_bool()
|
|
477
|
+
error = CError()
|
|
478
|
+
result = _lib.jasonisnthappy_is_read_only(self._db, ctypes.byref(read_only), ctypes.byref(error))
|
|
479
|
+
|
|
480
|
+
if result != 0:
|
|
481
|
+
_check_error(error)
|
|
482
|
+
raise RuntimeError("Failed to check read-only status")
|
|
483
|
+
|
|
484
|
+
return read_only.value
|
|
485
|
+
|
|
486
|
+
def max_bulk_operations(self) -> int:
|
|
487
|
+
"""Returns the maximum number of bulk operations allowed."""
|
|
488
|
+
if not self._db:
|
|
489
|
+
raise RuntimeError("Database is closed")
|
|
490
|
+
|
|
491
|
+
error = CError()
|
|
492
|
+
result = _lib.jasonisnthappy_max_bulk_operations(self._db, ctypes.byref(error))
|
|
493
|
+
return result
|
|
494
|
+
|
|
495
|
+
def max_document_size(self) -> int:
|
|
496
|
+
"""Returns the maximum document size in bytes."""
|
|
497
|
+
if not self._db:
|
|
498
|
+
raise RuntimeError("Database is closed")
|
|
499
|
+
|
|
500
|
+
error = CError()
|
|
501
|
+
result = _lib.jasonisnthappy_max_document_size(self._db, ctypes.byref(error))
|
|
502
|
+
return result
|
|
503
|
+
|
|
504
|
+
def max_request_body_size(self) -> int:
|
|
505
|
+
"""Returns the maximum HTTP request body size in bytes."""
|
|
506
|
+
if not self._db:
|
|
507
|
+
raise RuntimeError("Database is closed")
|
|
508
|
+
|
|
509
|
+
error = CError()
|
|
510
|
+
result = _lib.jasonisnthappy_max_request_body_size(self._db, ctypes.byref(error))
|
|
511
|
+
return result
|
|
512
|
+
|
|
513
|
+
def list_collections(self) -> List[str]:
|
|
514
|
+
"""Lists all collections in the database."""
|
|
515
|
+
if not self._db:
|
|
516
|
+
raise RuntimeError("Database is closed")
|
|
517
|
+
|
|
518
|
+
json_out = ctypes.c_char_p()
|
|
519
|
+
error = CError()
|
|
520
|
+
result = _lib.jasonisnthappy_list_collections(self._db, ctypes.byref(json_out), ctypes.byref(error))
|
|
521
|
+
|
|
522
|
+
if result != 0:
|
|
523
|
+
_check_error(error)
|
|
524
|
+
raise RuntimeError("Failed to list collections")
|
|
525
|
+
|
|
526
|
+
json_str = json_out.value.decode("utf-8")
|
|
527
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
528
|
+
return json.loads(json_str)
|
|
529
|
+
|
|
530
|
+
def collection_stats(self, collection_name: str) -> Dict[str, Any]:
|
|
531
|
+
"""Gets statistics for a collection."""
|
|
532
|
+
if not self._db:
|
|
533
|
+
raise RuntimeError("Database is closed")
|
|
534
|
+
|
|
535
|
+
json_out = ctypes.c_char_p()
|
|
536
|
+
error = CError()
|
|
537
|
+
result = _lib.jasonisnthappy_collection_stats(
|
|
538
|
+
self._db,
|
|
539
|
+
collection_name.encode("utf-8"),
|
|
540
|
+
ctypes.byref(json_out),
|
|
541
|
+
ctypes.byref(error)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
if result != 0:
|
|
545
|
+
_check_error(error)
|
|
546
|
+
raise RuntimeError("Failed to get collection stats")
|
|
547
|
+
|
|
548
|
+
json_str = json_out.value.decode("utf-8")
|
|
549
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
550
|
+
return json.loads(json_str)
|
|
551
|
+
|
|
552
|
+
def database_info(self) -> Dict[str, Any]:
|
|
553
|
+
"""Gets database information."""
|
|
554
|
+
if not self._db:
|
|
555
|
+
raise RuntimeError("Database is closed")
|
|
556
|
+
|
|
557
|
+
json_out = ctypes.c_char_p()
|
|
558
|
+
error = CError()
|
|
559
|
+
result = _lib.jasonisnthappy_database_info(self._db, ctypes.byref(json_out), ctypes.byref(error))
|
|
560
|
+
|
|
561
|
+
if result != 0:
|
|
562
|
+
_check_error(error)
|
|
563
|
+
raise RuntimeError("Failed to get database info")
|
|
564
|
+
|
|
565
|
+
json_str = json_out.value.decode("utf-8")
|
|
566
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
567
|
+
return json.loads(json_str)
|
|
568
|
+
|
|
569
|
+
# Index Management
|
|
570
|
+
def list_indexes(self, collection_name: str) -> List[Dict[str, Any]]:
|
|
571
|
+
"""Lists all indexes for a collection."""
|
|
572
|
+
if not self._db:
|
|
573
|
+
raise RuntimeError("Database is closed")
|
|
574
|
+
|
|
575
|
+
json_out = ctypes.c_char_p()
|
|
576
|
+
error = CError()
|
|
577
|
+
result = _lib.jasonisnthappy_list_indexes(
|
|
578
|
+
self._db,
|
|
579
|
+
collection_name.encode("utf-8"),
|
|
580
|
+
ctypes.byref(json_out),
|
|
581
|
+
ctypes.byref(error)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if result != 0:
|
|
585
|
+
_check_error(error)
|
|
586
|
+
raise RuntimeError("Failed to list indexes")
|
|
587
|
+
|
|
588
|
+
json_str = json_out.value.decode("utf-8")
|
|
589
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
590
|
+
return json.loads(json_str)
|
|
591
|
+
|
|
592
|
+
def create_index(self, collection_name: str, index_name: str, field: str, unique: bool = False) -> None:
|
|
593
|
+
"""Creates a single-field index."""
|
|
594
|
+
if not self._db:
|
|
595
|
+
raise RuntimeError("Database is closed")
|
|
596
|
+
|
|
597
|
+
error = CError()
|
|
598
|
+
result = _lib.jasonisnthappy_create_index(
|
|
599
|
+
self._db,
|
|
600
|
+
collection_name.encode("utf-8"),
|
|
601
|
+
index_name.encode("utf-8"),
|
|
602
|
+
field.encode("utf-8"),
|
|
603
|
+
unique,
|
|
604
|
+
ctypes.byref(error)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if result != 0:
|
|
608
|
+
_check_error(error)
|
|
609
|
+
raise RuntimeError("Failed to create index")
|
|
610
|
+
|
|
611
|
+
def create_compound_index(self, collection_name: str, index_name: str, fields: List[str], unique: bool = False) -> None:
|
|
612
|
+
"""Creates a compound index on multiple fields."""
|
|
613
|
+
if not self._db:
|
|
614
|
+
raise RuntimeError("Database is closed")
|
|
615
|
+
|
|
616
|
+
fields_json = json.dumps(fields)
|
|
617
|
+
error = CError()
|
|
618
|
+
result = _lib.jasonisnthappy_create_compound_index(
|
|
619
|
+
self._db,
|
|
620
|
+
collection_name.encode("utf-8"),
|
|
621
|
+
index_name.encode("utf-8"),
|
|
622
|
+
fields_json.encode("utf-8"),
|
|
623
|
+
unique,
|
|
624
|
+
ctypes.byref(error)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if result != 0:
|
|
628
|
+
_check_error(error)
|
|
629
|
+
raise RuntimeError("Failed to create compound index")
|
|
630
|
+
|
|
631
|
+
def create_text_index(self, collection_name: str, index_name: str, field: str) -> None:
|
|
632
|
+
"""Creates a full-text search index."""
|
|
633
|
+
if not self._db:
|
|
634
|
+
raise RuntimeError("Database is closed")
|
|
635
|
+
|
|
636
|
+
error = CError()
|
|
637
|
+
result = _lib.jasonisnthappy_create_text_index(
|
|
638
|
+
self._db,
|
|
639
|
+
collection_name.encode("utf-8"),
|
|
640
|
+
index_name.encode("utf-8"),
|
|
641
|
+
field.encode("utf-8"),
|
|
642
|
+
ctypes.byref(error)
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if result != 0:
|
|
646
|
+
_check_error(error)
|
|
647
|
+
raise RuntimeError("Failed to create text index")
|
|
648
|
+
|
|
649
|
+
def drop_index(self, collection_name: str, index_name: str) -> None:
|
|
650
|
+
"""Drops an index (stub implementation)."""
|
|
651
|
+
if not self._db:
|
|
652
|
+
raise RuntimeError("Database is closed")
|
|
653
|
+
|
|
654
|
+
error = CError()
|
|
655
|
+
result = _lib.jasonisnthappy_drop_index(
|
|
656
|
+
self._db,
|
|
657
|
+
collection_name.encode("utf-8"),
|
|
658
|
+
index_name.encode("utf-8"),
|
|
659
|
+
ctypes.byref(error)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
if result != 0:
|
|
663
|
+
_check_error(error)
|
|
664
|
+
raise RuntimeError("Failed to drop index")
|
|
665
|
+
|
|
666
|
+
# Schema Validation
|
|
667
|
+
def set_schema(self, collection_name: str, schema: Dict[str, Any]) -> None:
|
|
668
|
+
"""Sets a JSON schema for validation."""
|
|
669
|
+
if not self._db:
|
|
670
|
+
raise RuntimeError("Database is closed")
|
|
671
|
+
|
|
672
|
+
schema_json = json.dumps(schema)
|
|
673
|
+
error = CError()
|
|
674
|
+
result = _lib.jasonisnthappy_set_schema(
|
|
675
|
+
self._db,
|
|
676
|
+
collection_name.encode("utf-8"),
|
|
677
|
+
schema_json.encode("utf-8"),
|
|
678
|
+
ctypes.byref(error)
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
if result != 0:
|
|
682
|
+
_check_error(error)
|
|
683
|
+
raise RuntimeError("Failed to set schema")
|
|
684
|
+
|
|
685
|
+
def get_schema(self, collection_name: str) -> Optional[Dict[str, Any]]:
|
|
686
|
+
"""Gets the JSON schema for a collection."""
|
|
687
|
+
if not self._db:
|
|
688
|
+
raise RuntimeError("Database is closed")
|
|
689
|
+
|
|
690
|
+
json_out = ctypes.c_char_p()
|
|
691
|
+
error = CError()
|
|
692
|
+
result = _lib.jasonisnthappy_get_schema(
|
|
693
|
+
self._db,
|
|
694
|
+
collection_name.encode("utf-8"),
|
|
695
|
+
ctypes.byref(json_out),
|
|
696
|
+
ctypes.byref(error)
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
if result != 0:
|
|
700
|
+
_check_error(error)
|
|
701
|
+
raise RuntimeError("Failed to get schema")
|
|
702
|
+
|
|
703
|
+
if not json_out.value:
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
json_str = json_out.value.decode("utf-8")
|
|
707
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
708
|
+
return json.loads(json_str)
|
|
709
|
+
|
|
710
|
+
def remove_schema(self, collection_name: str) -> None:
|
|
711
|
+
"""Removes the JSON schema from a collection."""
|
|
712
|
+
if not self._db:
|
|
713
|
+
raise RuntimeError("Database is closed")
|
|
714
|
+
|
|
715
|
+
error = CError()
|
|
716
|
+
result = _lib.jasonisnthappy_remove_schema(
|
|
717
|
+
self._db,
|
|
718
|
+
collection_name.encode("utf-8"),
|
|
719
|
+
ctypes.byref(error)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
if result != 0:
|
|
723
|
+
_check_error(error)
|
|
724
|
+
raise RuntimeError("Failed to remove schema")
|
|
725
|
+
|
|
726
|
+
# Maintenance
|
|
727
|
+
def checkpoint(self) -> None:
|
|
728
|
+
"""Performs a manual WAL checkpoint."""
|
|
729
|
+
if not self._db:
|
|
730
|
+
raise RuntimeError("Database is closed")
|
|
731
|
+
|
|
732
|
+
error = CError()
|
|
733
|
+
result = _lib.jasonisnthappy_checkpoint(self._db, ctypes.byref(error))
|
|
734
|
+
|
|
735
|
+
if result != 0:
|
|
736
|
+
_check_error(error)
|
|
737
|
+
raise RuntimeError("Failed to checkpoint")
|
|
738
|
+
|
|
739
|
+
def backup(self, dest_path: str) -> None:
|
|
740
|
+
"""Creates a backup of the database."""
|
|
741
|
+
if not self._db:
|
|
742
|
+
raise RuntimeError("Database is closed")
|
|
743
|
+
|
|
744
|
+
error = CError()
|
|
745
|
+
result = _lib.jasonisnthappy_backup(self._db, dest_path.encode("utf-8"), ctypes.byref(error))
|
|
746
|
+
|
|
747
|
+
if result != 0:
|
|
748
|
+
_check_error(error)
|
|
749
|
+
raise RuntimeError("Failed to backup")
|
|
750
|
+
|
|
751
|
+
def verify_backup(self, backup_path: str) -> Dict[str, Any]:
|
|
752
|
+
"""Verifies the integrity of a backup and returns backup info."""
|
|
753
|
+
if not self._db:
|
|
754
|
+
raise RuntimeError("Database is closed")
|
|
755
|
+
|
|
756
|
+
json_out = ctypes.c_char_p()
|
|
757
|
+
error = CError()
|
|
758
|
+
result = _lib.jasonisnthappy_verify_backup(
|
|
759
|
+
self._db,
|
|
760
|
+
backup_path.encode("utf-8"),
|
|
761
|
+
ctypes.byref(json_out),
|
|
762
|
+
ctypes.byref(error)
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if result != 0:
|
|
766
|
+
_check_error(error)
|
|
767
|
+
raise RuntimeError("Failed to verify backup")
|
|
768
|
+
|
|
769
|
+
json_str = json_out.value.decode("utf-8")
|
|
770
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
771
|
+
return json.loads(json_str)
|
|
772
|
+
|
|
773
|
+
def garbage_collect(self) -> Dict[str, Any]:
|
|
774
|
+
"""Performs garbage collection and returns stats."""
|
|
775
|
+
if not self._db:
|
|
776
|
+
raise RuntimeError("Database is closed")
|
|
777
|
+
|
|
778
|
+
json_out = ctypes.c_char_p()
|
|
779
|
+
error = CError()
|
|
780
|
+
result = _lib.jasonisnthappy_garbage_collect(self._db, ctypes.byref(json_out), ctypes.byref(error))
|
|
781
|
+
|
|
782
|
+
if result != 0:
|
|
783
|
+
_check_error(error)
|
|
784
|
+
raise RuntimeError("Failed to garbage collect")
|
|
785
|
+
|
|
786
|
+
json_str = json_out.value.decode("utf-8")
|
|
787
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
788
|
+
return json.loads(json_str)
|
|
789
|
+
|
|
790
|
+
def metrics(self) -> Dict[str, Any]:
|
|
791
|
+
"""Gets database metrics."""
|
|
792
|
+
if not self._db:
|
|
793
|
+
raise RuntimeError("Database is closed")
|
|
794
|
+
|
|
795
|
+
json_out = ctypes.c_char_p()
|
|
796
|
+
error = CError()
|
|
797
|
+
result = _lib.jasonisnthappy_metrics(self._db, ctypes.byref(json_out), ctypes.byref(error))
|
|
798
|
+
|
|
799
|
+
if result != 0:
|
|
800
|
+
_check_error(error)
|
|
801
|
+
raise RuntimeError("Failed to get metrics")
|
|
802
|
+
|
|
803
|
+
json_str = json_out.value.decode("utf-8")
|
|
804
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
805
|
+
return json.loads(json_str)
|
|
806
|
+
|
|
807
|
+
def frame_count(self) -> int:
|
|
808
|
+
"""Gets the number of WAL frames."""
|
|
809
|
+
if not self._db:
|
|
810
|
+
raise RuntimeError("Database is closed")
|
|
811
|
+
|
|
812
|
+
count = ctypes.c_uint64()
|
|
813
|
+
error = CError()
|
|
814
|
+
result = _lib.jasonisnthappy_frame_count(self._db, ctypes.byref(count), ctypes.byref(error))
|
|
815
|
+
|
|
816
|
+
if result != 0:
|
|
817
|
+
_check_error(error)
|
|
818
|
+
raise RuntimeError("Failed to get frame count")
|
|
819
|
+
|
|
820
|
+
return count.value
|
|
821
|
+
|
|
822
|
+
# Transaction Operations
|
|
823
|
+
def begin_transaction(self) -> "Transaction":
|
|
824
|
+
"""Begins a new transaction."""
|
|
825
|
+
if not self._db:
|
|
826
|
+
raise RuntimeError("Database is closed")
|
|
827
|
+
|
|
828
|
+
error = CError()
|
|
829
|
+
tx_ptr = _lib.jasonisnthappy_begin_transaction(self._db, ctypes.byref(error))
|
|
830
|
+
|
|
831
|
+
if not tx_ptr:
|
|
832
|
+
_check_error(error)
|
|
833
|
+
raise RuntimeError("Failed to begin transaction")
|
|
834
|
+
|
|
835
|
+
return Transaction(tx_ptr)
|
|
836
|
+
|
|
837
|
+
def get_collection(self, name: str) -> "Collection":
|
|
838
|
+
"""Gets a collection reference for non-transactional operations."""
|
|
839
|
+
if not self._db:
|
|
840
|
+
raise RuntimeError("Database is closed")
|
|
841
|
+
|
|
842
|
+
error = CError()
|
|
843
|
+
coll_ptr = _lib.jasonisnthappy_get_collection(self._db, name.encode("utf-8"), ctypes.byref(error))
|
|
844
|
+
|
|
845
|
+
if not coll_ptr:
|
|
846
|
+
_check_error(error)
|
|
847
|
+
raise RuntimeError("Failed to get collection")
|
|
848
|
+
|
|
849
|
+
return Collection(coll_ptr)
|
|
850
|
+
|
|
851
|
+
def start_web_ui(self, addr: str) -> "WebServer":
|
|
852
|
+
"""
|
|
853
|
+
Starts the web UI server at the given address.
|
|
854
|
+
|
|
855
|
+
Returns a WebServer handle that can be used to stop the server.
|
|
856
|
+
|
|
857
|
+
Example:
|
|
858
|
+
>>> server = db.start_web_ui("127.0.0.1:8080")
|
|
859
|
+
>>> print("Web UI available at http://127.0.0.1:8080")
|
|
860
|
+
>>> # ... later ...
|
|
861
|
+
>>> server.stop()
|
|
862
|
+
"""
|
|
863
|
+
if not self._db:
|
|
864
|
+
raise RuntimeError("Database is closed")
|
|
865
|
+
|
|
866
|
+
error = CError()
|
|
867
|
+
server_ptr = _lib.jasonisnthappy_start_web_server(
|
|
868
|
+
self._db, addr.encode("utf-8"), ctypes.byref(error)
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
if not server_ptr:
|
|
872
|
+
_check_error(error)
|
|
873
|
+
raise RuntimeError("Failed to start web server")
|
|
874
|
+
|
|
875
|
+
return WebServer(server_ptr)
|
|
876
|
+
|
|
877
|
+
def __enter__(self) -> "Database":
|
|
878
|
+
return self
|
|
879
|
+
|
|
880
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
881
|
+
self.close()
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
# ==================
|
|
885
|
+
# WebServer Class
|
|
886
|
+
# ==================
|
|
887
|
+
|
|
888
|
+
class WebServer:
|
|
889
|
+
"""
|
|
890
|
+
WebServer represents a running web UI server.
|
|
891
|
+
|
|
892
|
+
Example:
|
|
893
|
+
>>> server = db.start_web_ui("127.0.0.1:8080")
|
|
894
|
+
>>> print("Web UI available at http://127.0.0.1:8080")
|
|
895
|
+
>>> # ... later ...
|
|
896
|
+
>>> server.stop()
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
def __init__(self, server_ptr):
|
|
900
|
+
self._server = server_ptr
|
|
901
|
+
|
|
902
|
+
def stop(self) -> None:
|
|
903
|
+
"""Stops the web server."""
|
|
904
|
+
if self._server:
|
|
905
|
+
_lib.jasonisnthappy_stop_web_server(self._server)
|
|
906
|
+
self._server = None
|
|
907
|
+
|
|
908
|
+
def __enter__(self) -> "WebServer":
|
|
909
|
+
return self
|
|
910
|
+
|
|
911
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
912
|
+
self.stop()
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# ==================
|
|
916
|
+
# Transaction Class
|
|
917
|
+
# ==================
|
|
918
|
+
|
|
919
|
+
class Transaction:
|
|
920
|
+
"""
|
|
921
|
+
Transaction represents a database transaction.
|
|
922
|
+
|
|
923
|
+
Example:
|
|
924
|
+
>>> tx = db.begin_transaction()
|
|
925
|
+
>>> try:
|
|
926
|
+
... doc_id = tx.insert("users", {"name": "Alice", "age": 30})
|
|
927
|
+
... doc = tx.find_by_id("users", doc_id)
|
|
928
|
+
... tx.commit()
|
|
929
|
+
... except Exception:
|
|
930
|
+
... tx.rollback()
|
|
931
|
+
... raise
|
|
932
|
+
"""
|
|
933
|
+
|
|
934
|
+
def __init__(self, tx_ptr: int):
|
|
935
|
+
self._tx = tx_ptr
|
|
936
|
+
|
|
937
|
+
def is_active(self) -> bool:
|
|
938
|
+
"""Checks if the transaction is still active."""
|
|
939
|
+
if not self._tx:
|
|
940
|
+
return False
|
|
941
|
+
|
|
942
|
+
error = CError()
|
|
943
|
+
result = _lib.jasonisnthappy_transaction_is_active(self._tx, ctypes.byref(error))
|
|
944
|
+
|
|
945
|
+
if result < 0:
|
|
946
|
+
_check_error(error)
|
|
947
|
+
raise RuntimeError("Failed to check transaction status")
|
|
948
|
+
|
|
949
|
+
return result == 1
|
|
950
|
+
|
|
951
|
+
def commit(self) -> None:
|
|
952
|
+
"""Commits the transaction."""
|
|
953
|
+
if not self._tx:
|
|
954
|
+
raise RuntimeError("Transaction is already closed")
|
|
955
|
+
|
|
956
|
+
error = CError()
|
|
957
|
+
result = _lib.jasonisnthappy_commit(self._tx, ctypes.byref(error))
|
|
958
|
+
self._tx = None
|
|
959
|
+
|
|
960
|
+
if result != 0:
|
|
961
|
+
_check_error(error)
|
|
962
|
+
raise RuntimeError("Failed to commit transaction")
|
|
963
|
+
|
|
964
|
+
def rollback(self) -> None:
|
|
965
|
+
"""Rolls back the transaction."""
|
|
966
|
+
if self._tx:
|
|
967
|
+
_lib.jasonisnthappy_rollback(self._tx)
|
|
968
|
+
self._tx = None
|
|
969
|
+
|
|
970
|
+
def insert(self, collection_name: str, doc: Dict[str, Any]) -> str:
|
|
971
|
+
"""Inserts a document into a collection."""
|
|
972
|
+
if not self._tx:
|
|
973
|
+
raise RuntimeError("Transaction is closed")
|
|
974
|
+
|
|
975
|
+
json_str = json.dumps(doc)
|
|
976
|
+
id_out = ctypes.c_char_p()
|
|
977
|
+
error = CError()
|
|
978
|
+
|
|
979
|
+
result = _lib.jasonisnthappy_insert(
|
|
980
|
+
self._tx,
|
|
981
|
+
collection_name.encode("utf-8"),
|
|
982
|
+
json_str.encode("utf-8"),
|
|
983
|
+
ctypes.byref(id_out),
|
|
984
|
+
ctypes.byref(error),
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
if result != 0:
|
|
988
|
+
_check_error(error)
|
|
989
|
+
raise RuntimeError("Failed to insert document")
|
|
990
|
+
|
|
991
|
+
doc_id = id_out.value.decode("utf-8")
|
|
992
|
+
_lib.jasonisnthappy_free_string(id_out)
|
|
993
|
+
return doc_id
|
|
994
|
+
|
|
995
|
+
def find_by_id(self, collection_name: str, doc_id: str) -> Optional[Dict[str, Any]]:
|
|
996
|
+
"""Finds a document by its ID."""
|
|
997
|
+
if not self._tx:
|
|
998
|
+
raise RuntimeError("Transaction is closed")
|
|
999
|
+
|
|
1000
|
+
json_out = ctypes.c_char_p()
|
|
1001
|
+
error = CError()
|
|
1002
|
+
|
|
1003
|
+
status = _lib.jasonisnthappy_find_by_id(
|
|
1004
|
+
self._tx,
|
|
1005
|
+
collection_name.encode("utf-8"),
|
|
1006
|
+
doc_id.encode("utf-8"),
|
|
1007
|
+
ctypes.byref(json_out),
|
|
1008
|
+
ctypes.byref(error),
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
if status == -1:
|
|
1012
|
+
_check_error(error)
|
|
1013
|
+
raise RuntimeError("Failed to find document")
|
|
1014
|
+
|
|
1015
|
+
if status == 1 or not json_out:
|
|
1016
|
+
return None
|
|
1017
|
+
|
|
1018
|
+
json_str = json_out.value.decode("utf-8")
|
|
1019
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1020
|
+
|
|
1021
|
+
return json.loads(json_str)
|
|
1022
|
+
|
|
1023
|
+
def update_by_id(self, collection_name: str, doc_id: str, doc: Dict[str, Any]) -> None:
|
|
1024
|
+
"""Updates a document by its ID."""
|
|
1025
|
+
if not self._tx:
|
|
1026
|
+
raise RuntimeError("Transaction is closed")
|
|
1027
|
+
|
|
1028
|
+
json_str = json.dumps(doc)
|
|
1029
|
+
error = CError()
|
|
1030
|
+
|
|
1031
|
+
result = _lib.jasonisnthappy_update_by_id(
|
|
1032
|
+
self._tx,
|
|
1033
|
+
collection_name.encode("utf-8"),
|
|
1034
|
+
doc_id.encode("utf-8"),
|
|
1035
|
+
json_str.encode("utf-8"),
|
|
1036
|
+
ctypes.byref(error),
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
if result != 0:
|
|
1040
|
+
_check_error(error)
|
|
1041
|
+
raise RuntimeError("Failed to update document")
|
|
1042
|
+
|
|
1043
|
+
def delete_by_id(self, collection_name: str, doc_id: str) -> None:
|
|
1044
|
+
"""Deletes a document by its ID."""
|
|
1045
|
+
if not self._tx:
|
|
1046
|
+
raise RuntimeError("Transaction is closed")
|
|
1047
|
+
|
|
1048
|
+
error = CError()
|
|
1049
|
+
result = _lib.jasonisnthappy_delete_by_id(
|
|
1050
|
+
self._tx,
|
|
1051
|
+
collection_name.encode("utf-8"),
|
|
1052
|
+
doc_id.encode("utf-8"),
|
|
1053
|
+
ctypes.byref(error),
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
if result != 0:
|
|
1057
|
+
_check_error(error)
|
|
1058
|
+
raise RuntimeError("Failed to delete document")
|
|
1059
|
+
|
|
1060
|
+
def find_all(self, collection_name: str) -> List[Dict[str, Any]]:
|
|
1061
|
+
"""Finds all documents in a collection."""
|
|
1062
|
+
if not self._tx:
|
|
1063
|
+
raise RuntimeError("Transaction is closed")
|
|
1064
|
+
|
|
1065
|
+
json_out = ctypes.c_char_p()
|
|
1066
|
+
error = CError()
|
|
1067
|
+
|
|
1068
|
+
status = _lib.jasonisnthappy_find_all(
|
|
1069
|
+
self._tx,
|
|
1070
|
+
collection_name.encode("utf-8"),
|
|
1071
|
+
ctypes.byref(json_out),
|
|
1072
|
+
ctypes.byref(error),
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
if status != 0:
|
|
1076
|
+
_check_error(error)
|
|
1077
|
+
raise RuntimeError("Failed to find all documents")
|
|
1078
|
+
|
|
1079
|
+
json_str = json_out.value.decode("utf-8")
|
|
1080
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1081
|
+
|
|
1082
|
+
return json.loads(json_str)
|
|
1083
|
+
|
|
1084
|
+
def count(self, collection_name: str) -> int:
|
|
1085
|
+
"""Counts documents in a collection."""
|
|
1086
|
+
if not self._tx:
|
|
1087
|
+
raise RuntimeError("Transaction is closed")
|
|
1088
|
+
|
|
1089
|
+
count = ctypes.c_uint64()
|
|
1090
|
+
error = CError()
|
|
1091
|
+
|
|
1092
|
+
result = _lib.jasonisnthappy_count(
|
|
1093
|
+
self._tx,
|
|
1094
|
+
collection_name.encode("utf-8"),
|
|
1095
|
+
ctypes.byref(count),
|
|
1096
|
+
ctypes.byref(error),
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
if result != 0:
|
|
1100
|
+
_check_error(error)
|
|
1101
|
+
raise RuntimeError("Failed to count documents")
|
|
1102
|
+
|
|
1103
|
+
return count.value
|
|
1104
|
+
|
|
1105
|
+
def create_collection(self, collection_name: str) -> None:
|
|
1106
|
+
"""Creates a new collection."""
|
|
1107
|
+
if not self._tx:
|
|
1108
|
+
raise RuntimeError("Transaction is closed")
|
|
1109
|
+
|
|
1110
|
+
error = CError()
|
|
1111
|
+
result = _lib.jasonisnthappy_create_collection(
|
|
1112
|
+
self._tx,
|
|
1113
|
+
collection_name.encode("utf-8"),
|
|
1114
|
+
ctypes.byref(error),
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
if result != 0:
|
|
1118
|
+
_check_error(error)
|
|
1119
|
+
raise RuntimeError("Failed to create collection")
|
|
1120
|
+
|
|
1121
|
+
def drop_collection(self, collection_name: str) -> None:
|
|
1122
|
+
"""Drops a collection."""
|
|
1123
|
+
if not self._tx:
|
|
1124
|
+
raise RuntimeError("Transaction is closed")
|
|
1125
|
+
|
|
1126
|
+
error = CError()
|
|
1127
|
+
result = _lib.jasonisnthappy_drop_collection(
|
|
1128
|
+
self._tx,
|
|
1129
|
+
collection_name.encode("utf-8"),
|
|
1130
|
+
ctypes.byref(error),
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
if result != 0:
|
|
1134
|
+
_check_error(error)
|
|
1135
|
+
raise RuntimeError("Failed to drop collection")
|
|
1136
|
+
|
|
1137
|
+
def rename_collection(self, old_name: str, new_name: str) -> None:
|
|
1138
|
+
"""Renames a collection."""
|
|
1139
|
+
if not self._tx:
|
|
1140
|
+
raise RuntimeError("Transaction is closed")
|
|
1141
|
+
|
|
1142
|
+
error = CError()
|
|
1143
|
+
result = _lib.jasonisnthappy_rename_collection(
|
|
1144
|
+
self._tx,
|
|
1145
|
+
old_name.encode("utf-8"),
|
|
1146
|
+
new_name.encode("utf-8"),
|
|
1147
|
+
ctypes.byref(error),
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
if result != 0:
|
|
1151
|
+
_check_error(error)
|
|
1152
|
+
raise RuntimeError("Failed to rename collection")
|
|
1153
|
+
|
|
1154
|
+
def __enter__(self) -> "Transaction":
|
|
1155
|
+
return self
|
|
1156
|
+
|
|
1157
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1158
|
+
if exc_type is None:
|
|
1159
|
+
self.commit()
|
|
1160
|
+
else:
|
|
1161
|
+
self.rollback()
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
# ==================
|
|
1165
|
+
# Collection Class
|
|
1166
|
+
# ==================
|
|
1167
|
+
|
|
1168
|
+
class Collection:
|
|
1169
|
+
"""
|
|
1170
|
+
Collection represents a non-transactional collection handle.
|
|
1171
|
+
|
|
1172
|
+
Example:
|
|
1173
|
+
>>> coll = db.get_collection("users")
|
|
1174
|
+
>>> try:
|
|
1175
|
+
... doc_id = coll.insert({"name": "Bob", "age": 25})
|
|
1176
|
+
... doc = coll.find_by_id(doc_id)
|
|
1177
|
+
... finally:
|
|
1178
|
+
... coll.close()
|
|
1179
|
+
"""
|
|
1180
|
+
|
|
1181
|
+
def __init__(self, coll_ptr: int):
|
|
1182
|
+
self._coll = coll_ptr
|
|
1183
|
+
|
|
1184
|
+
def close(self) -> None:
|
|
1185
|
+
"""Frees the collection handle."""
|
|
1186
|
+
if self._coll:
|
|
1187
|
+
_lib.jasonisnthappy_collection_free(self._coll)
|
|
1188
|
+
self._coll = None
|
|
1189
|
+
|
|
1190
|
+
def name(self) -> str:
|
|
1191
|
+
"""Gets the collection name."""
|
|
1192
|
+
if not self._coll:
|
|
1193
|
+
raise RuntimeError("Collection is closed")
|
|
1194
|
+
|
|
1195
|
+
name_out = ctypes.c_char_p()
|
|
1196
|
+
error = CError()
|
|
1197
|
+
result = _lib.jasonisnthappy_collection_name(self._coll, ctypes.byref(name_out), ctypes.byref(error))
|
|
1198
|
+
|
|
1199
|
+
if result != 0:
|
|
1200
|
+
_check_error(error)
|
|
1201
|
+
raise RuntimeError("Failed to get collection name")
|
|
1202
|
+
|
|
1203
|
+
name = name_out.value.decode("utf-8")
|
|
1204
|
+
_lib.jasonisnthappy_free_string(name_out)
|
|
1205
|
+
return name
|
|
1206
|
+
|
|
1207
|
+
# Basic CRUD
|
|
1208
|
+
def insert(self, doc: Dict[str, Any]) -> str:
|
|
1209
|
+
"""Inserts a document."""
|
|
1210
|
+
if not self._coll:
|
|
1211
|
+
raise RuntimeError("Collection is closed")
|
|
1212
|
+
|
|
1213
|
+
json_str = json.dumps(doc)
|
|
1214
|
+
id_out = ctypes.c_char_p()
|
|
1215
|
+
error = CError()
|
|
1216
|
+
|
|
1217
|
+
result = _lib.jasonisnthappy_collection_insert(
|
|
1218
|
+
self._coll,
|
|
1219
|
+
json_str.encode("utf-8"),
|
|
1220
|
+
ctypes.byref(id_out),
|
|
1221
|
+
ctypes.byref(error),
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
if result != 0:
|
|
1225
|
+
_check_error(error)
|
|
1226
|
+
raise RuntimeError("Failed to insert document")
|
|
1227
|
+
|
|
1228
|
+
doc_id = id_out.value.decode("utf-8")
|
|
1229
|
+
_lib.jasonisnthappy_free_string(id_out)
|
|
1230
|
+
return doc_id
|
|
1231
|
+
|
|
1232
|
+
def find_by_id(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
|
1233
|
+
"""Finds a document by ID."""
|
|
1234
|
+
if not self._coll:
|
|
1235
|
+
raise RuntimeError("Collection is closed")
|
|
1236
|
+
|
|
1237
|
+
json_out = ctypes.c_char_p()
|
|
1238
|
+
error = CError()
|
|
1239
|
+
|
|
1240
|
+
status = _lib.jasonisnthappy_collection_find_by_id(
|
|
1241
|
+
self._coll,
|
|
1242
|
+
doc_id.encode("utf-8"),
|
|
1243
|
+
ctypes.byref(json_out),
|
|
1244
|
+
ctypes.byref(error),
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
if status == -1:
|
|
1248
|
+
_check_error(error)
|
|
1249
|
+
raise RuntimeError("Failed to find document")
|
|
1250
|
+
|
|
1251
|
+
if status == 1 or not json_out:
|
|
1252
|
+
return None
|
|
1253
|
+
|
|
1254
|
+
json_str = json_out.value.decode("utf-8")
|
|
1255
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1256
|
+
return json.loads(json_str)
|
|
1257
|
+
|
|
1258
|
+
def update_by_id(self, doc_id: str, doc: Dict[str, Any]) -> None:
|
|
1259
|
+
"""Updates a document by ID."""
|
|
1260
|
+
if not self._coll:
|
|
1261
|
+
raise RuntimeError("Collection is closed")
|
|
1262
|
+
|
|
1263
|
+
json_str = json.dumps(doc)
|
|
1264
|
+
error = CError()
|
|
1265
|
+
|
|
1266
|
+
result = _lib.jasonisnthappy_collection_update_by_id(
|
|
1267
|
+
self._coll,
|
|
1268
|
+
doc_id.encode("utf-8"),
|
|
1269
|
+
json_str.encode("utf-8"),
|
|
1270
|
+
ctypes.byref(error),
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
if result != 0:
|
|
1274
|
+
_check_error(error)
|
|
1275
|
+
raise RuntimeError("Failed to update document")
|
|
1276
|
+
|
|
1277
|
+
def delete_by_id(self, doc_id: str) -> None:
|
|
1278
|
+
"""Deletes a document by ID."""
|
|
1279
|
+
if not self._coll:
|
|
1280
|
+
raise RuntimeError("Collection is closed")
|
|
1281
|
+
|
|
1282
|
+
error = CError()
|
|
1283
|
+
result = _lib.jasonisnthappy_collection_delete_by_id(
|
|
1284
|
+
self._coll,
|
|
1285
|
+
doc_id.encode("utf-8"),
|
|
1286
|
+
ctypes.byref(error),
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
if result != 0:
|
|
1290
|
+
_check_error(error)
|
|
1291
|
+
raise RuntimeError("Failed to delete document")
|
|
1292
|
+
|
|
1293
|
+
def find_all(self) -> List[Dict[str, Any]]:
|
|
1294
|
+
"""Finds all documents."""
|
|
1295
|
+
if not self._coll:
|
|
1296
|
+
raise RuntimeError("Collection is closed")
|
|
1297
|
+
|
|
1298
|
+
json_out = ctypes.c_char_p()
|
|
1299
|
+
error = CError()
|
|
1300
|
+
|
|
1301
|
+
status = _lib.jasonisnthappy_collection_find_all(
|
|
1302
|
+
self._coll,
|
|
1303
|
+
ctypes.byref(json_out),
|
|
1304
|
+
ctypes.byref(error),
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
if status != 0:
|
|
1308
|
+
_check_error(error)
|
|
1309
|
+
raise RuntimeError("Failed to find all documents")
|
|
1310
|
+
|
|
1311
|
+
json_str = json_out.value.decode("utf-8")
|
|
1312
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1313
|
+
return json.loads(json_str)
|
|
1314
|
+
|
|
1315
|
+
def count(self) -> int:
|
|
1316
|
+
"""Counts all documents."""
|
|
1317
|
+
if not self._coll:
|
|
1318
|
+
raise RuntimeError("Collection is closed")
|
|
1319
|
+
|
|
1320
|
+
count = ctypes.c_uint64()
|
|
1321
|
+
error = CError()
|
|
1322
|
+
|
|
1323
|
+
result = _lib.jasonisnthappy_collection_count(
|
|
1324
|
+
self._coll,
|
|
1325
|
+
ctypes.byref(count),
|
|
1326
|
+
ctypes.byref(error),
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
if result != 0:
|
|
1330
|
+
_check_error(error)
|
|
1331
|
+
raise RuntimeError("Failed to count documents")
|
|
1332
|
+
|
|
1333
|
+
return count.value
|
|
1334
|
+
|
|
1335
|
+
# Query/Filter Operations
|
|
1336
|
+
def find(self, filter_str: str) -> List[Dict[str, Any]]:
|
|
1337
|
+
"""Finds documents matching a filter."""
|
|
1338
|
+
if not self._coll:
|
|
1339
|
+
raise RuntimeError("Collection is closed")
|
|
1340
|
+
|
|
1341
|
+
json_out = ctypes.c_char_p()
|
|
1342
|
+
error = CError()
|
|
1343
|
+
|
|
1344
|
+
status = _lib.jasonisnthappy_collection_find(
|
|
1345
|
+
self._coll,
|
|
1346
|
+
filter_str.encode("utf-8"),
|
|
1347
|
+
ctypes.byref(json_out),
|
|
1348
|
+
ctypes.byref(error),
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
if status != 0:
|
|
1352
|
+
_check_error(error)
|
|
1353
|
+
raise RuntimeError("Failed to find documents")
|
|
1354
|
+
|
|
1355
|
+
json_str = json_out.value.decode("utf-8")
|
|
1356
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1357
|
+
return json.loads(json_str)
|
|
1358
|
+
|
|
1359
|
+
def find_one(self, filter_str: str) -> Optional[Dict[str, Any]]:
|
|
1360
|
+
"""Finds first document matching a filter."""
|
|
1361
|
+
if not self._coll:
|
|
1362
|
+
raise RuntimeError("Collection is closed")
|
|
1363
|
+
|
|
1364
|
+
json_out = ctypes.c_char_p()
|
|
1365
|
+
error = CError()
|
|
1366
|
+
|
|
1367
|
+
status = _lib.jasonisnthappy_collection_find_one(
|
|
1368
|
+
self._coll,
|
|
1369
|
+
filter_str.encode("utf-8"),
|
|
1370
|
+
ctypes.byref(json_out),
|
|
1371
|
+
ctypes.byref(error),
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
if status != 0:
|
|
1375
|
+
_check_error(error)
|
|
1376
|
+
raise RuntimeError("Failed to find document")
|
|
1377
|
+
|
|
1378
|
+
if not json_out or not json_out.value:
|
|
1379
|
+
return None
|
|
1380
|
+
|
|
1381
|
+
json_str = json_out.value.decode("utf-8")
|
|
1382
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1383
|
+
|
|
1384
|
+
if json_str == "null" or not json_str:
|
|
1385
|
+
return None
|
|
1386
|
+
|
|
1387
|
+
return json.loads(json_str)
|
|
1388
|
+
|
|
1389
|
+
def update(self, filter_str: str, update: Dict[str, Any]) -> int:
|
|
1390
|
+
"""Updates all documents matching a filter. Returns count updated."""
|
|
1391
|
+
if not self._coll:
|
|
1392
|
+
raise RuntimeError("Collection is closed")
|
|
1393
|
+
|
|
1394
|
+
update_json = json.dumps(update)
|
|
1395
|
+
count = ctypes.c_uint64()
|
|
1396
|
+
error = CError()
|
|
1397
|
+
|
|
1398
|
+
result = _lib.jasonisnthappy_collection_update(
|
|
1399
|
+
self._coll,
|
|
1400
|
+
filter_str.encode("utf-8"),
|
|
1401
|
+
update_json.encode("utf-8"),
|
|
1402
|
+
ctypes.byref(count),
|
|
1403
|
+
ctypes.byref(error),
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
if result != 0:
|
|
1407
|
+
_check_error(error)
|
|
1408
|
+
raise RuntimeError("Failed to update documents")
|
|
1409
|
+
|
|
1410
|
+
return count.value
|
|
1411
|
+
|
|
1412
|
+
def update_one(self, filter_str: str, update: Dict[str, Any]) -> bool:
|
|
1413
|
+
"""Updates first document matching a filter. Returns True if updated."""
|
|
1414
|
+
if not self._coll:
|
|
1415
|
+
raise RuntimeError("Collection is closed")
|
|
1416
|
+
|
|
1417
|
+
update_json = json.dumps(update)
|
|
1418
|
+
updated = ctypes.c_bool()
|
|
1419
|
+
error = CError()
|
|
1420
|
+
|
|
1421
|
+
result = _lib.jasonisnthappy_collection_update_one(
|
|
1422
|
+
self._coll,
|
|
1423
|
+
filter_str.encode("utf-8"),
|
|
1424
|
+
update_json.encode("utf-8"),
|
|
1425
|
+
ctypes.byref(updated),
|
|
1426
|
+
ctypes.byref(error),
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
if result != 0:
|
|
1430
|
+
_check_error(error)
|
|
1431
|
+
raise RuntimeError("Failed to update document")
|
|
1432
|
+
|
|
1433
|
+
return updated.value
|
|
1434
|
+
|
|
1435
|
+
def delete(self, filter_str: str) -> int:
|
|
1436
|
+
"""Deletes all documents matching a filter. Returns count deleted."""
|
|
1437
|
+
if not self._coll:
|
|
1438
|
+
raise RuntimeError("Collection is closed")
|
|
1439
|
+
|
|
1440
|
+
count = ctypes.c_uint64()
|
|
1441
|
+
error = CError()
|
|
1442
|
+
|
|
1443
|
+
result = _lib.jasonisnthappy_collection_delete(
|
|
1444
|
+
self._coll,
|
|
1445
|
+
filter_str.encode("utf-8"),
|
|
1446
|
+
ctypes.byref(count),
|
|
1447
|
+
ctypes.byref(error),
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
if result != 0:
|
|
1451
|
+
_check_error(error)
|
|
1452
|
+
raise RuntimeError("Failed to delete documents")
|
|
1453
|
+
|
|
1454
|
+
return count.value
|
|
1455
|
+
|
|
1456
|
+
def delete_one(self, filter_str: str) -> bool:
|
|
1457
|
+
"""Deletes first document matching a filter. Returns True if deleted."""
|
|
1458
|
+
if not self._coll:
|
|
1459
|
+
raise RuntimeError("Collection is closed")
|
|
1460
|
+
|
|
1461
|
+
deleted = ctypes.c_bool()
|
|
1462
|
+
error = CError()
|
|
1463
|
+
|
|
1464
|
+
result = _lib.jasonisnthappy_collection_delete_one(
|
|
1465
|
+
self._coll,
|
|
1466
|
+
filter_str.encode("utf-8"),
|
|
1467
|
+
ctypes.byref(deleted),
|
|
1468
|
+
ctypes.byref(error),
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
if result != 0:
|
|
1472
|
+
_check_error(error)
|
|
1473
|
+
raise RuntimeError("Failed to delete document")
|
|
1474
|
+
|
|
1475
|
+
return deleted.value
|
|
1476
|
+
|
|
1477
|
+
# Upsert Operations
|
|
1478
|
+
def upsert_by_id(self, doc_id: str, doc: Dict[str, Any]) -> UpsertResult:
|
|
1479
|
+
"""Upserts a document by ID. Returns UpsertResult with id and inserted flag."""
|
|
1480
|
+
if not self._coll:
|
|
1481
|
+
raise RuntimeError("Collection is closed")
|
|
1482
|
+
|
|
1483
|
+
doc_json = json.dumps(doc)
|
|
1484
|
+
result_code = ctypes.c_int32()
|
|
1485
|
+
id_out = ctypes.c_char_p()
|
|
1486
|
+
error = CError()
|
|
1487
|
+
|
|
1488
|
+
status = _lib.jasonisnthappy_collection_upsert_by_id(
|
|
1489
|
+
self._coll,
|
|
1490
|
+
doc_id.encode("utf-8"),
|
|
1491
|
+
doc_json.encode("utf-8"),
|
|
1492
|
+
ctypes.byref(result_code),
|
|
1493
|
+
ctypes.byref(id_out),
|
|
1494
|
+
ctypes.byref(error),
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
if status != 0:
|
|
1498
|
+
_check_error(error)
|
|
1499
|
+
raise RuntimeError("Failed to upsert document")
|
|
1500
|
+
|
|
1501
|
+
result_id = id_out.value.decode("utf-8")
|
|
1502
|
+
_lib.jasonisnthappy_free_string(id_out)
|
|
1503
|
+
# result_code: 0 = Inserted, 1 = Updated
|
|
1504
|
+
return UpsertResult(id=result_id, inserted=(result_code.value == 0))
|
|
1505
|
+
|
|
1506
|
+
def upsert(self, filter_str: str, doc: Dict[str, Any]) -> UpsertResult:
|
|
1507
|
+
"""Upserts a document matching a filter. Returns UpsertResult with id and inserted flag."""
|
|
1508
|
+
if not self._coll:
|
|
1509
|
+
raise RuntimeError("Collection is closed")
|
|
1510
|
+
|
|
1511
|
+
doc_json = json.dumps(doc)
|
|
1512
|
+
result_code = ctypes.c_int32()
|
|
1513
|
+
id_out = ctypes.c_char_p()
|
|
1514
|
+
error = CError()
|
|
1515
|
+
|
|
1516
|
+
status = _lib.jasonisnthappy_collection_upsert(
|
|
1517
|
+
self._coll,
|
|
1518
|
+
filter_str.encode("utf-8"),
|
|
1519
|
+
doc_json.encode("utf-8"),
|
|
1520
|
+
ctypes.byref(result_code),
|
|
1521
|
+
ctypes.byref(id_out),
|
|
1522
|
+
ctypes.byref(error),
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
if status != 0:
|
|
1526
|
+
_check_error(error)
|
|
1527
|
+
raise RuntimeError("Failed to upsert document")
|
|
1528
|
+
|
|
1529
|
+
result_id = id_out.value.decode("utf-8")
|
|
1530
|
+
_lib.jasonisnthappy_free_string(id_out)
|
|
1531
|
+
# result_code: 0 = Inserted, 1 = Updated
|
|
1532
|
+
return UpsertResult(id=result_id, inserted=(result_code.value == 0))
|
|
1533
|
+
|
|
1534
|
+
# Bulk Operations
|
|
1535
|
+
def insert_many(self, docs: List[Dict[str, Any]]) -> List[str]:
|
|
1536
|
+
"""Inserts multiple documents. Returns list of IDs."""
|
|
1537
|
+
if not self._coll:
|
|
1538
|
+
raise RuntimeError("Collection is closed")
|
|
1539
|
+
|
|
1540
|
+
docs_json = json.dumps(docs)
|
|
1541
|
+
ids_out = ctypes.c_char_p()
|
|
1542
|
+
error = CError()
|
|
1543
|
+
|
|
1544
|
+
result = _lib.jasonisnthappy_collection_insert_many(
|
|
1545
|
+
self._coll,
|
|
1546
|
+
docs_json.encode("utf-8"),
|
|
1547
|
+
ctypes.byref(ids_out),
|
|
1548
|
+
ctypes.byref(error),
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
if result != 0:
|
|
1552
|
+
_check_error(error)
|
|
1553
|
+
raise RuntimeError("Failed to insert documents")
|
|
1554
|
+
|
|
1555
|
+
ids_str = ids_out.value.decode("utf-8")
|
|
1556
|
+
_lib.jasonisnthappy_free_string(ids_out)
|
|
1557
|
+
return json.loads(ids_str)
|
|
1558
|
+
|
|
1559
|
+
# Advanced Operations
|
|
1560
|
+
def distinct(self, field: str) -> List[Any]:
|
|
1561
|
+
"""Gets distinct values for a field."""
|
|
1562
|
+
if not self._coll:
|
|
1563
|
+
raise RuntimeError("Collection is closed")
|
|
1564
|
+
|
|
1565
|
+
json_out = ctypes.c_char_p()
|
|
1566
|
+
error = CError()
|
|
1567
|
+
|
|
1568
|
+
result = _lib.jasonisnthappy_collection_distinct(
|
|
1569
|
+
self._coll,
|
|
1570
|
+
field.encode("utf-8"),
|
|
1571
|
+
ctypes.byref(json_out),
|
|
1572
|
+
ctypes.byref(error),
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
if result != 0:
|
|
1576
|
+
_check_error(error)
|
|
1577
|
+
raise RuntimeError("Failed to get distinct values")
|
|
1578
|
+
|
|
1579
|
+
json_str = json_out.value.decode("utf-8")
|
|
1580
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1581
|
+
return json.loads(json_str)
|
|
1582
|
+
|
|
1583
|
+
def count_distinct(self, field: str) -> int:
|
|
1584
|
+
"""Counts distinct values for a field."""
|
|
1585
|
+
if not self._coll:
|
|
1586
|
+
raise RuntimeError("Collection is closed")
|
|
1587
|
+
|
|
1588
|
+
count = ctypes.c_uint64()
|
|
1589
|
+
error = CError()
|
|
1590
|
+
|
|
1591
|
+
result = _lib.jasonisnthappy_collection_count_distinct(
|
|
1592
|
+
self._coll,
|
|
1593
|
+
field.encode("utf-8"),
|
|
1594
|
+
ctypes.byref(count),
|
|
1595
|
+
ctypes.byref(error),
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
if result != 0:
|
|
1599
|
+
_check_error(error)
|
|
1600
|
+
raise RuntimeError("Failed to count distinct values")
|
|
1601
|
+
|
|
1602
|
+
return count.value
|
|
1603
|
+
|
|
1604
|
+
def search(self, query: str) -> List[Dict[str, Any]]:
|
|
1605
|
+
"""Performs full-text search."""
|
|
1606
|
+
if not self._coll:
|
|
1607
|
+
raise RuntimeError("Collection is closed")
|
|
1608
|
+
|
|
1609
|
+
json_out = ctypes.c_char_p()
|
|
1610
|
+
error = CError()
|
|
1611
|
+
|
|
1612
|
+
status = _lib.jasonisnthappy_collection_search(
|
|
1613
|
+
self._coll,
|
|
1614
|
+
query.encode("utf-8"),
|
|
1615
|
+
ctypes.byref(json_out),
|
|
1616
|
+
ctypes.byref(error),
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
if status != 0:
|
|
1620
|
+
_check_error(error)
|
|
1621
|
+
raise RuntimeError("Failed to search")
|
|
1622
|
+
|
|
1623
|
+
json_str = json_out.value.decode("utf-8")
|
|
1624
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1625
|
+
return json.loads(json_str)
|
|
1626
|
+
|
|
1627
|
+
def count_with_query(self, filter_str: str) -> int:
|
|
1628
|
+
"""Counts documents matching a filter."""
|
|
1629
|
+
if not self._coll:
|
|
1630
|
+
raise RuntimeError("Collection is closed")
|
|
1631
|
+
|
|
1632
|
+
count = ctypes.c_uint64()
|
|
1633
|
+
error = CError()
|
|
1634
|
+
|
|
1635
|
+
result = _lib.jasonisnthappy_collection_count_with_query(
|
|
1636
|
+
self._coll,
|
|
1637
|
+
filter_str.encode("utf-8"),
|
|
1638
|
+
ctypes.byref(count),
|
|
1639
|
+
ctypes.byref(error),
|
|
1640
|
+
)
|
|
1641
|
+
|
|
1642
|
+
if result != 0:
|
|
1643
|
+
_check_error(error)
|
|
1644
|
+
raise RuntimeError("Failed to count documents")
|
|
1645
|
+
|
|
1646
|
+
return count.value
|
|
1647
|
+
|
|
1648
|
+
# Query Builder Helpers
|
|
1649
|
+
def query_with_options(
|
|
1650
|
+
self,
|
|
1651
|
+
filter_str: Optional[str] = None,
|
|
1652
|
+
sort_field: Optional[str] = None,
|
|
1653
|
+
sort_asc: bool = True,
|
|
1654
|
+
limit: int = 0,
|
|
1655
|
+
skip: int = 0,
|
|
1656
|
+
project_fields: Optional[List[str]] = None,
|
|
1657
|
+
exclude_fields: Optional[List[str]] = None,
|
|
1658
|
+
) -> List[Dict[str, Any]]:
|
|
1659
|
+
"""Executes a query with all options."""
|
|
1660
|
+
if not self._coll:
|
|
1661
|
+
raise RuntimeError("Collection is closed")
|
|
1662
|
+
|
|
1663
|
+
filter_c = filter_str.encode("utf-8") if filter_str else None
|
|
1664
|
+
sort_c = sort_field.encode("utf-8") if sort_field else None
|
|
1665
|
+
project_c = json.dumps(project_fields).encode("utf-8") if project_fields else None
|
|
1666
|
+
exclude_c = json.dumps(exclude_fields).encode("utf-8") if exclude_fields else None
|
|
1667
|
+
|
|
1668
|
+
json_out = ctypes.c_char_p()
|
|
1669
|
+
error = CError()
|
|
1670
|
+
|
|
1671
|
+
status = _lib.jasonisnthappy_collection_query_with_options(
|
|
1672
|
+
self._coll,
|
|
1673
|
+
filter_c,
|
|
1674
|
+
sort_c,
|
|
1675
|
+
sort_asc,
|
|
1676
|
+
limit,
|
|
1677
|
+
skip,
|
|
1678
|
+
project_c,
|
|
1679
|
+
exclude_c,
|
|
1680
|
+
ctypes.byref(json_out),
|
|
1681
|
+
ctypes.byref(error),
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
if status != 0:
|
|
1685
|
+
_check_error(error)
|
|
1686
|
+
raise RuntimeError("Failed to query documents")
|
|
1687
|
+
|
|
1688
|
+
json_str = json_out.value.decode("utf-8")
|
|
1689
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1690
|
+
return json.loads(json_str)
|
|
1691
|
+
|
|
1692
|
+
def query_count(self, filter_str: Optional[str] = None, skip: int = 0, limit: int = 0) -> int:
|
|
1693
|
+
"""Counts documents with query options."""
|
|
1694
|
+
if not self._coll:
|
|
1695
|
+
raise RuntimeError("Collection is closed")
|
|
1696
|
+
|
|
1697
|
+
filter_c = filter_str.encode("utf-8") if filter_str else None
|
|
1698
|
+
count = ctypes.c_size_t()
|
|
1699
|
+
error = CError()
|
|
1700
|
+
|
|
1701
|
+
result = _lib.jasonisnthappy_collection_query_count(
|
|
1702
|
+
self._coll,
|
|
1703
|
+
filter_c,
|
|
1704
|
+
skip,
|
|
1705
|
+
limit,
|
|
1706
|
+
ctypes.byref(count),
|
|
1707
|
+
ctypes.byref(error),
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
if result != 0:
|
|
1711
|
+
_check_error(error)
|
|
1712
|
+
raise RuntimeError("Failed to count documents")
|
|
1713
|
+
|
|
1714
|
+
return count.value
|
|
1715
|
+
|
|
1716
|
+
def query_first(
|
|
1717
|
+
self,
|
|
1718
|
+
filter_str: Optional[str] = None,
|
|
1719
|
+
sort_field: Optional[str] = None,
|
|
1720
|
+
sort_asc: bool = True,
|
|
1721
|
+
) -> Optional[Dict[str, Any]]:
|
|
1722
|
+
"""Gets the first document matching a query."""
|
|
1723
|
+
if not self._coll:
|
|
1724
|
+
raise RuntimeError("Collection is closed")
|
|
1725
|
+
|
|
1726
|
+
filter_c = filter_str.encode("utf-8") if filter_str else None
|
|
1727
|
+
sort_c = sort_field.encode("utf-8") if sort_field else None
|
|
1728
|
+
json_out = ctypes.c_char_p()
|
|
1729
|
+
error = CError()
|
|
1730
|
+
|
|
1731
|
+
status = _lib.jasonisnthappy_collection_query_first(
|
|
1732
|
+
self._coll,
|
|
1733
|
+
filter_c,
|
|
1734
|
+
sort_c,
|
|
1735
|
+
sort_asc,
|
|
1736
|
+
ctypes.byref(json_out),
|
|
1737
|
+
ctypes.byref(error),
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
if status != 0:
|
|
1741
|
+
_check_error(error)
|
|
1742
|
+
raise RuntimeError("Failed to query document")
|
|
1743
|
+
|
|
1744
|
+
if not json_out or not json_out.value:
|
|
1745
|
+
return None
|
|
1746
|
+
|
|
1747
|
+
json_str = json_out.value.decode("utf-8")
|
|
1748
|
+
_lib.jasonisnthappy_free_string(json_out)
|
|
1749
|
+
|
|
1750
|
+
if json_str == "null" or not json_str:
|
|
1751
|
+
return None
|
|
1752
|
+
|
|
1753
|
+
return json.loads(json_str)
|
|
1754
|
+
|
|
1755
|
+
# Bulk Write
|
|
1756
|
+
def bulk_write(self, operations: List[Dict[str, Any]], ordered: bool = True) -> Dict[str, Any]:
|
|
1757
|
+
"""Executes multiple operations in a transaction."""
|
|
1758
|
+
if not self._coll:
|
|
1759
|
+
raise RuntimeError("Collection is closed")
|
|
1760
|
+
|
|
1761
|
+
ops_json = json.dumps(operations)
|
|
1762
|
+
result_out = ctypes.c_char_p()
|
|
1763
|
+
error = CError()
|
|
1764
|
+
|
|
1765
|
+
status = _lib.jasonisnthappy_collection_bulk_write(
|
|
1766
|
+
self._coll,
|
|
1767
|
+
ops_json.encode("utf-8"),
|
|
1768
|
+
ordered,
|
|
1769
|
+
ctypes.byref(result_out),
|
|
1770
|
+
ctypes.byref(error),
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
if status != 0:
|
|
1774
|
+
_check_error(error)
|
|
1775
|
+
raise RuntimeError("Failed to execute bulk write")
|
|
1776
|
+
|
|
1777
|
+
result_str = result_out.value.decode("utf-8")
|
|
1778
|
+
_lib.jasonisnthappy_free_string(result_out)
|
|
1779
|
+
return json.loads(result_str)
|
|
1780
|
+
|
|
1781
|
+
# Aggregation
|
|
1782
|
+
def aggregate(self, pipeline: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1783
|
+
"""Executes an aggregation pipeline."""
|
|
1784
|
+
if not self._coll:
|
|
1785
|
+
raise RuntimeError("Collection is closed")
|
|
1786
|
+
|
|
1787
|
+
pipeline_json = json.dumps(pipeline)
|
|
1788
|
+
result_out = ctypes.c_char_p()
|
|
1789
|
+
error = CError()
|
|
1790
|
+
|
|
1791
|
+
status = _lib.jasonisnthappy_collection_aggregate(
|
|
1792
|
+
self._coll,
|
|
1793
|
+
pipeline_json.encode("utf-8"),
|
|
1794
|
+
ctypes.byref(result_out),
|
|
1795
|
+
ctypes.byref(error),
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
if status != 0:
|
|
1799
|
+
_check_error(error)
|
|
1800
|
+
raise RuntimeError("Failed to execute aggregation")
|
|
1801
|
+
|
|
1802
|
+
result_str = result_out.value.decode("utf-8")
|
|
1803
|
+
_lib.jasonisnthappy_free_string(result_out)
|
|
1804
|
+
return json.loads(result_str)
|
|
1805
|
+
|
|
1806
|
+
# Watch / Change Streams
|
|
1807
|
+
def watch(
|
|
1808
|
+
self,
|
|
1809
|
+
callback: Callable[[str, str, Optional[Dict[str, Any]]], None],
|
|
1810
|
+
filter_str: Optional[str] = None,
|
|
1811
|
+
) -> "WatchHandle":
|
|
1812
|
+
"""
|
|
1813
|
+
Starts watching for changes on the collection.
|
|
1814
|
+
|
|
1815
|
+
The callback receives (operation, doc_id, document) where:
|
|
1816
|
+
- operation: "insert", "update", or "delete"
|
|
1817
|
+
- doc_id: The document ID
|
|
1818
|
+
- document: The document data (None for delete operations)
|
|
1819
|
+
|
|
1820
|
+
Example:
|
|
1821
|
+
>>> def on_change(op, doc_id, doc):
|
|
1822
|
+
... print(f"{op}: {doc_id}")
|
|
1823
|
+
>>> handle = collection.watch(on_change)
|
|
1824
|
+
>>> # ... later ...
|
|
1825
|
+
>>> handle.stop()
|
|
1826
|
+
"""
|
|
1827
|
+
if not self._coll:
|
|
1828
|
+
raise RuntimeError("Collection is closed")
|
|
1829
|
+
|
|
1830
|
+
# Create the callback wrapper that will be called from C
|
|
1831
|
+
def c_callback(collection, operation, doc_id, doc_json, user_data):
|
|
1832
|
+
try:
|
|
1833
|
+
op_str = operation.decode("utf-8") if operation else ""
|
|
1834
|
+
id_str = doc_id.decode("utf-8") if doc_id else ""
|
|
1835
|
+
doc = json.loads(doc_json.decode("utf-8")) if doc_json else None
|
|
1836
|
+
callback(op_str, id_str, doc)
|
|
1837
|
+
except Exception:
|
|
1838
|
+
pass # Silently ignore callback errors
|
|
1839
|
+
|
|
1840
|
+
# Wrap the callback to prevent garbage collection
|
|
1841
|
+
c_callback_wrapped = WatchCallbackType(c_callback)
|
|
1842
|
+
|
|
1843
|
+
filter_c = filter_str.encode("utf-8") if filter_str else None
|
|
1844
|
+
handle_out = ctypes.c_void_p()
|
|
1845
|
+
error = CError()
|
|
1846
|
+
|
|
1847
|
+
status = _lib.jasonisnthappy_collection_watch_start(
|
|
1848
|
+
self._coll,
|
|
1849
|
+
filter_c,
|
|
1850
|
+
c_callback_wrapped,
|
|
1851
|
+
None, # user_data not needed since we use closure
|
|
1852
|
+
ctypes.byref(handle_out),
|
|
1853
|
+
ctypes.byref(error),
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
if status != 0:
|
|
1857
|
+
_check_error(error)
|
|
1858
|
+
raise RuntimeError("Failed to start watch")
|
|
1859
|
+
|
|
1860
|
+
return WatchHandle(handle_out, c_callback_wrapped)
|
|
1861
|
+
|
|
1862
|
+
def __enter__(self) -> "Collection":
|
|
1863
|
+
return self
|
|
1864
|
+
|
|
1865
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1866
|
+
self.close()
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
# ==================
|
|
1870
|
+
# WatchHandle Class
|
|
1871
|
+
# ==================
|
|
1872
|
+
|
|
1873
|
+
class WatchHandle:
|
|
1874
|
+
"""
|
|
1875
|
+
WatchHandle represents an active watch operation.
|
|
1876
|
+
|
|
1877
|
+
Example:
|
|
1878
|
+
>>> def on_change(op, doc_id, doc):
|
|
1879
|
+
... print(f"{op}: {doc_id}")
|
|
1880
|
+
>>> handle = collection.watch(on_change)
|
|
1881
|
+
>>> # ... later ...
|
|
1882
|
+
>>> handle.stop()
|
|
1883
|
+
"""
|
|
1884
|
+
|
|
1885
|
+
def __init__(self, handle_ptr, callback_ref):
|
|
1886
|
+
self._handle = handle_ptr
|
|
1887
|
+
# Keep a reference to the callback to prevent garbage collection
|
|
1888
|
+
self._callback_ref = callback_ref
|
|
1889
|
+
|
|
1890
|
+
def stop(self) -> None:
|
|
1891
|
+
"""Stops watching and cleans up resources."""
|
|
1892
|
+
if self._handle:
|
|
1893
|
+
_lib.jasonisnthappy_watch_stop(self._handle)
|
|
1894
|
+
self._handle = None
|
|
1895
|
+
self._callback_ref = None
|
|
1896
|
+
|
|
1897
|
+
def __enter__(self) -> "WatchHandle":
|
|
1898
|
+
return self
|
|
1899
|
+
|
|
1900
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1901
|
+
self.stop()
|