jasonisnthappy 0.1.0__py3-none-manylinux2014_aarch64.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.
@@ -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()