wavedb 0.1.8__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wavedb/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """WaveDB Python bindings."""
2
+
3
+ from .config import WaveDBConfig, WaveDBEncryption
4
+ from .database import WaveDB
5
+ from .exceptions import (
6
+ EncryptionError,
7
+ GraphQLLayerError,
8
+ IOError_,
9
+ InvalidPathError,
10
+ NotFoundError,
11
+ WaveDBError,
12
+ )
13
+ from .graph_layer import GraphLayer, GraphQuery, GraphResult
14
+ from .graphql_layer import GraphQLLayer, GraphQLError, GraphQLResult
15
+ from .subtree import Subtree
16
+
17
+ __version__ = "0.1.8"
18
+
19
+ __all__ = [
20
+ "EncryptionError",
21
+ "GraphLayer",
22
+ "GraphQuery",
23
+ "GraphResult",
24
+ "GraphQLError",
25
+ "GraphQLLayer",
26
+ "GraphQLLayerError",
27
+ "GraphQLResult",
28
+ "IOError_",
29
+ "InvalidPathError",
30
+ "NotFoundError",
31
+ "Subtree",
32
+ "WaveDB",
33
+ "WaveDBError",
34
+ "WaveDBConfig",
35
+ "WaveDBEncryption",
36
+ ]
wavedb/_async.py ADDED
@@ -0,0 +1,314 @@
1
+ """Bridge between C promise_t callbacks and Python asyncio futures.
2
+
3
+ The C library invokes promise callbacks on worker threads. We must not touch
4
+ asyncio.Future from a foreign thread. Instead, the cffi callback schedules
5
+ _deliver on the future's loop via loop.call_soon_threadsafe.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import itertools
11
+ import logging
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from ._errors import map_error
16
+ from ._native import ffi, lib, libc
17
+ from .exceptions import WaveDBError
18
+
19
+
20
+ _log = logging.getLogger("wavedb.async")
21
+
22
+
23
+ def decode_identifier_payload(payload) -> "bytes | None":
24
+ """Decode the identifier_t* payload from database_get_raw /
25
+ database_subtree_get_raw.
26
+
27
+ Payload contract (verified against src/Database/database.c):
28
+ - Hit: identifier_t* with count=1, yield=1 (CONSUME'd by _database_get
29
+ before promise_resolve).
30
+ - Miss: NULL.
31
+
32
+ Decoding sequence:
33
+ 1. identifier_get_data_copy -> malloc'd uint8_t* (freed with
34
+ database_raw_value_free).
35
+ 2. Copy to Python bytes.
36
+ 3. refcounter_reference -> consumes the yield without incrementing
37
+ count.
38
+ 4. identifier_destroy -> decrements count to 0 and frees the
39
+ identifier.
40
+
41
+ REFERENCE is mandatory before destroy: identifier_destroy calls
42
+ refcounter_dereference which consumes the yield first — without
43
+ REFERENCE, the identifier would be leaked.
44
+ """
45
+ if payload == ffi.NULL:
46
+ return None
47
+ ident = ffi.cast("identifier_t*", payload)
48
+ try:
49
+ len_out = ffi.new("size_t*")
50
+ data_ptr = lib.identifier_get_data_copy(ident, len_out)
51
+ if data_ptr == ffi.NULL:
52
+ return None
53
+ try:
54
+ return ffi.buffer(data_ptr, len_out[0])[:]
55
+ finally:
56
+ lib.database_raw_value_free(data_ptr)
57
+ finally:
58
+ lib.refcounter_reference(ffi.cast("void*", ident))
59
+ lib.identifier_destroy(ident)
60
+
61
+
62
+ @dataclass
63
+ class PendingOp:
64
+ op_id: int
65
+ future: asyncio.Future
66
+ loop: asyncio.AbstractEventLoop
67
+ op_type: str
68
+ op_id_ptr: Any # cffi-allocated intptr_t*; bridge owns lifetime via _all_op_id_ptrs
69
+ cancelled: bool = False
70
+
71
+
72
+ class AsyncBridge:
73
+ """Per-database bridge. One instance per WaveDB connection."""
74
+
75
+ # Class-level: op-ids grow monotonically across connections; only meaningful
76
+ # within a bridge's _pending dict.
77
+ _id_counter = itertools.count(1)
78
+
79
+ def __init__(self) -> None:
80
+ self._pending: dict[int, PendingOp] = {}
81
+ # The bridge owns op_id_ptr lifetime for safety across any calling
82
+ # pattern. Each entry is 8 bytes; the high-water mark of concurrent ops
83
+ # bounds the count. Never cleared — freed when the bridge is GC'd
84
+ # (after database_destroy has drained the work pool and all promises
85
+ # are destroyed).
86
+ self._all_op_id_ptrs: list[Any] = []
87
+ # Shared cffi callbacks (kept alive at instance scope; never GC'd while
88
+ # C holds a pointer).
89
+ self._resolve_cb = ffi.callback("void(void*, void*)")(self._on_resolve)
90
+ self._reject_cb = ffi.callback("void(void*, async_error_t*)")(self._on_reject)
91
+
92
+ # ---- public API ----
93
+
94
+ def new_future(self, op_type: str = "unknown") -> tuple[asyncio.Future, int, Any]:
95
+ """Create a future + op-id pair. Returns (future, op_id, op_id_ptr).
96
+
97
+ Caller passes op_id_ptr as promise_create's ctx. Caller must call
98
+ register_pending() with the same op_id before invoking the C async fn.
99
+ """
100
+ loop = asyncio.get_running_loop()
101
+ fut: asyncio.Future = loop.create_future()
102
+ op_id = next(self._id_counter)
103
+ op_id_ptr = ffi.new("intptr_t*", op_id)
104
+ self._all_op_id_ptrs.append(op_id_ptr) # bridge owns the lifetime
105
+ return fut, op_id, op_id_ptr
106
+
107
+ def register_pending(self, fut: asyncio.Future, op_id: int, op_id_ptr: Any, op_type: str) -> None:
108
+ loop = fut.get_loop()
109
+ self._pending[op_id] = PendingOp(
110
+ op_id=op_id, future=fut, loop=loop, op_type=op_type, op_id_ptr=op_id_ptr,
111
+ )
112
+ # Store op_id on the future for O(1) unregister without a reverse dict.
113
+ fut._wavedb_op_id = op_id # type: ignore[attr-defined]
114
+
115
+ def dispatch(
116
+ self,
117
+ c_fn,
118
+ handle_ptr: Any,
119
+ key_b: bytes,
120
+ delimiter_byte: bytes,
121
+ op_type: str,
122
+ err_msg: str,
123
+ *extra_args,
124
+ ) -> tuple[asyncio.Future, Any]:
125
+ """Create a promise, register the pending op, call the C async function.
126
+
127
+ `handle_ptr` is the C handle (database_t* or database_subtree_t*) that
128
+ the C function is invoked on. `extra_args` are passed between the
129
+ delimiter and the promise in the C function call (e.g.,
130
+ value_b, len(value_b) for put).
131
+
132
+ Returns (fut, promise). On error the promise is destroyed and the
133
+ mapped exception is raised. On success the caller must promise_destroy
134
+ in a finally block after awaiting the future.
135
+ """
136
+ fut, op_id, op_id_ptr = self.new_future(op_type)
137
+ self.register_pending(fut, op_id, op_id_ptr, op_type)
138
+ promise = lib.promise_create(self._resolve_cb, self._reject_cb, op_id_ptr)
139
+ if promise == ffi.NULL:
140
+ # Clean up the orphaned pending op so it doesn't sit in _pending
141
+ # with a future that will never resolve.
142
+ self._pending.pop(op_id, None)
143
+ raise WaveDBError("promise_create failed")
144
+ try:
145
+ rc = c_fn(
146
+ handle_ptr, ffi.from_buffer(key_b), len(key_b),
147
+ delimiter_byte, *extra_args, promise,
148
+ )
149
+ if rc != 0:
150
+ raise map_error(rc, err_msg)
151
+ except BaseException:
152
+ lib.promise_destroy(promise)
153
+ self._pending.pop(op_id, None)
154
+ raise
155
+ return fut, promise
156
+
157
+ @property
158
+ def resolve_cb(self):
159
+ return self._resolve_cb
160
+
161
+ @property
162
+ def reject_cb(self):
163
+ return self._reject_cb
164
+
165
+ def cancel_all_pending(self) -> None:
166
+ for op in list(self._pending.values()):
167
+ op.cancelled = True
168
+ if not op.future.done():
169
+ try:
170
+ op.future.cancel()
171
+ except (RuntimeError, asyncio.InvalidStateError):
172
+ pass
173
+ self._pending.clear()
174
+
175
+ # ---- test helpers (also usable for non-C scheduling) ----
176
+
177
+ def schedule_resolve(self, fut: asyncio.Future, payload: Any) -> None:
178
+ """Schedule a resolve on the future's loop. Used by tests."""
179
+ loop = fut.get_loop()
180
+ try:
181
+ loop.call_soon_threadsafe(self._deliver, fut, payload, False)
182
+ except RuntimeError:
183
+ # Loop closed between scheduling and delivery — drop silently.
184
+ _log.warning("schedule_resolve on closed loop; dropping result")
185
+
186
+ def schedule_reject(self, fut: asyncio.Future, exception: BaseException) -> None:
187
+ """Schedule a rejection on the future's loop. Used by tests."""
188
+ loop = fut.get_loop()
189
+ try:
190
+ loop.call_soon_threadsafe(self._set_exception, fut, exception)
191
+ except RuntimeError:
192
+ _log.warning("schedule_reject on closed loop; dropping exception")
193
+
194
+ # ---- internal: cffi callbacks (run on C worker threads) ----
195
+
196
+ def _on_resolve(self, ctx: Any, payload: Any) -> None:
197
+ # Runs on C worker thread. GIL is held (cffi callback acquires it).
198
+ if ctx == ffi.NULL:
199
+ return
200
+ op_id_ptr = ffi.cast("intptr_t*", ctx)
201
+ op_id = int(op_id_ptr[0])
202
+ op = self._pending.get(op_id)
203
+ if op is None:
204
+ # Op was cancelled before C fired the callback. Free the payload
205
+ # if non-NULL — get ops resolve with identifier_t* (leaks if not
206
+ # freed); put/del/batch always resolve with NULL (no-op).
207
+ if payload != ffi.NULL:
208
+ _free_get_payload_if_applicable(payload, "get")
209
+ return
210
+ op_type = op.op_type
211
+ try:
212
+ op.loop.call_soon_threadsafe(self._deliver, op.future, payload, False, op_type)
213
+ except RuntimeError:
214
+ _log.warning("_on_resolve on closed loop; dropping result for op %d", op_id)
215
+ # Loop is closed — free the payload if it's a get-type identifier.
216
+ _free_get_payload_if_applicable(payload, op_type)
217
+
218
+ def _on_reject(self, ctx: Any, error: Any) -> None:
219
+ if ctx == ffi.NULL:
220
+ if error != ffi.NULL:
221
+ lib.error_destroy(error)
222
+ return
223
+ op_id_ptr = ffi.cast("intptr_t*", ctx)
224
+ op_id = int(op_id_ptr[0])
225
+ op = self._pending.get(op_id)
226
+ # Extract message and destroy the error object now (it's refcounted by C
227
+ # but we don't keep a reference once we've read the string).
228
+ msg = ""
229
+ if error != ffi.NULL:
230
+ cmsg = lib.error_get_message(error)
231
+ if cmsg != ffi.NULL:
232
+ msg = ffi.string(cmsg).decode("utf-8", errors="replace")
233
+ # Enrich with C source location (file:function:line) for debugging
234
+ cfile = lib.error_get_file(error)
235
+ cfunc = lib.error_get_function(error)
236
+ line = lib.error_get_line(error)
237
+ if cfile != ffi.NULL and cfunc != ffi.NULL:
238
+ loc = f" ({ffi.string(cfile).decode('utf-8', errors='replace')}:{line} in {ffi.string(cfunc).decode('utf-8', errors='replace')})"
239
+ msg = f"{msg}{loc}"
240
+ lib.error_destroy(error)
241
+ if op is None:
242
+ return
243
+ try:
244
+ op.loop.call_soon_threadsafe(self._deliver_error, op.future, msg)
245
+ except RuntimeError:
246
+ _log.warning("_on_reject on closed loop; dropping error for op %d", op_id)
247
+
248
+ # ---- internal: loop-thread delivery ----
249
+
250
+ def _deliver(self, fut: asyncio.Future, payload: Any, loop_closed: bool = False, op_type: str = "unknown") -> None:
251
+ # Runs on the loop thread (or directly in tests with loop_closed=True).
252
+ # loop_closed is a test-only escape hatch; in production,
253
+ # call_soon_threadsafe raises RuntimeError on a closed loop before
254
+ # _deliver is invoked.
255
+ if loop_closed or fut.done():
256
+ # Cancelled/closed before delivery. Free the payload if it's a
257
+ # get-type op (identifier_t* with yield=1 from CONSUME). put/del/batch
258
+ # resolve with NULL, so there's nothing to free.
259
+ _free_get_payload_if_applicable(payload, op_type)
260
+ return
261
+ fut.set_result(payload)
262
+ self._unregister(fut)
263
+
264
+ def _deliver_error(self, fut: asyncio.Future, message: str) -> None:
265
+ if fut.done():
266
+ return
267
+ fut.set_exception(map_error(0, message))
268
+ self._unregister(fut)
269
+
270
+ def _set_exception(self, fut: asyncio.Future, exception: BaseException) -> None:
271
+ """Used by schedule_reject (test path) to set an arbitrary exception."""
272
+ if fut.done():
273
+ return
274
+ fut.set_exception(exception)
275
+ self._unregister(fut)
276
+
277
+ def _unregister(self, fut: asyncio.Future) -> None:
278
+ op_id = getattr(fut, '_wavedb_op_id', None)
279
+ if op_id is not None:
280
+ self._pending.pop(op_id, None)
281
+ # Do NOT free op_id_ptr — the bridge owns it via _all_op_id_ptrs
282
+ # (see I-1) and keeps it alive until database_destroy has drained.
283
+
284
+
285
+ # Op types whose promise payload is an identifier_t* (CONSUME'd by _database_get).
286
+ # These need refcounter_reference + identifier_destroy to free when the future
287
+ # is cancelled before delivery. Batch resolves with malloc'd int* (the result
288
+ # code) which must be freed with libc.free on cancellation.
289
+ _GET_OP_TYPES = frozenset({"get", "st_get"})
290
+ _BATCH_OP_TYPES = frozenset({"batch"})
291
+
292
+
293
+ def _free_get_payload_if_applicable(payload: Any, op_type: str) -> None:
294
+ """Free a cancelled op payload (identifier_t* for get, int* for batch).
295
+
296
+ No-op for op types with NULL payloads. Safe to call from any thread
297
+ (cffi callbacks hold the GIL).
298
+ """
299
+ if payload == ffi.NULL:
300
+ return
301
+ if op_type in _GET_OP_TYPES:
302
+ try:
303
+ ident = ffi.cast("identifier_t*", payload)
304
+ lib.refcounter_reference(ffi.cast("void*", ident))
305
+ lib.identifier_destroy(ident)
306
+ except Exception:
307
+ _log.warning("failed to free cancelled get payload", exc_info=True)
308
+ return
309
+ if op_type in _BATCH_OP_TYPES:
310
+ try:
311
+ libc.free(payload)
312
+ except Exception:
313
+ _log.warning("failed to free cancelled batch payload", exc_info=True)
314
+ return
wavedb/_cffi_build.py ADDED
@@ -0,0 +1,269 @@
1
+ """Build-time cffi builder for compiled out-of-line mode.
2
+
3
+ Defines all C API cdef declarations and provides a configure() function
4
+ that setup.py calls with build-time parameters (library_dirs, rpath, etc.)
5
+ before invoking ffibuilder.compile().
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from cffi import FFI
14
+
15
+
16
+ ffibuilder = FFI()
17
+
18
+ # Minimal C preamble for set_source — includes standard headers for the
19
+ # types used in the cdef (uint8_t, size_t, etc.). Opaque struct typedefs
20
+ # are generated by cffi from the cdef; no real WaveDB headers needed.
21
+ # raw_op_t and raw_result_t are fully declared here (not in cdef) so cffi
22
+ # uses this definition for field access without generating a conflicting one.
23
+ _C_SOURCE = """
24
+ #include <stdint.h>
25
+ #include <stddef.h>
26
+
27
+ /* Forward-declare ALL opaque types used in function signatures.
28
+ cffi generates the actual typedefs later in the file, but the
29
+ _cffi_d_ check functions at the top reference the type names
30
+ before the definitions appear. Forward-declaring here avoids
31
+ "unknown type name" errors. These must match the cdef exactly. */
32
+ typedef struct refcounter_s refcounter_t;
33
+ typedef struct database_s database_t;
34
+ typedef struct database_config_s database_config_t;
35
+ typedef struct encrypted_database_config_s encrypted_database_config_t;
36
+ typedef struct promise_s promise_t;
37
+ typedef struct async_error_s async_error_t;
38
+ typedef struct database_iterator_s database_iterator_t;
39
+ typedef struct path_s path_t;
40
+ typedef struct identifier_s identifier_t;
41
+ typedef struct database_subtree_s database_subtree_t;
42
+ typedef struct graph_layer_config_s graph_layer_config_t;
43
+ typedef struct graph_layer_s graph_layer_t;
44
+ typedef struct graph_query_s graph_query_t;
45
+ typedef struct graph_result_s graph_result_t;
46
+ typedef struct graphql_layer_config_s graphql_layer_config_t;
47
+ typedef struct graphql_layer_s graphql_layer_t;
48
+ typedef struct graphql_result_s graphql_result_t;
49
+
50
+ /* Fully-declared structs (fields accessible from Python).
51
+ cffi uses this definition for field access via the `...` form in cdef. */
52
+ typedef struct { const char* key; size_t key_len; const uint8_t* value; size_t value_len; int type; } raw_op_t;
53
+ typedef struct { char* key; size_t key_len; uint8_t* value; size_t value_len; } raw_result_t;
54
+ """
55
+
56
+ # ---- Types ----
57
+ # All WaveDB structs are declared opaque via forward declarations.
58
+ # Works identically in API mode — cffi emits the typedef into the generated
59
+ # C file; no real header required.
60
+ ffibuilder.cdef("""
61
+ typedef struct refcounter_s refcounter_t;
62
+ typedef struct database_s database_t;
63
+ typedef struct database_config_s database_config_t;
64
+ typedef struct encrypted_database_config_s encrypted_database_config_t;
65
+ typedef struct promise_s promise_t;
66
+ typedef struct async_error_s async_error_t;
67
+ typedef struct database_iterator_s database_iterator_t;
68
+ typedef struct path_s path_t;
69
+ typedef struct identifier_s identifier_t;
70
+ typedef struct database_subtree_s database_subtree_t;
71
+ typedef struct graph_layer_config_s graph_layer_config_t;
72
+ typedef struct graph_layer_s graph_layer_t;
73
+ typedef struct graph_query_s graph_query_t;
74
+ typedef struct graph_result_s graph_result_t;
75
+ typedef struct graphql_layer_config_s graphql_layer_config_t;
76
+ typedef struct graphql_layer_s graphql_layer_t;
77
+ typedef struct graphql_result_s graphql_result_t;
78
+
79
+ typedef struct { ...; } raw_op_t;
80
+ typedef struct { ...; } raw_result_t;
81
+ """)
82
+
83
+ ffibuilder.cdef("""
84
+ uint8_t* identifier_get_data_copy(const identifier_t* id, size_t* out_len);
85
+ void identifier_destroy(identifier_t* id);
86
+ void* refcounter_reference(void* refcounter);
87
+ """)
88
+
89
+ ffibuilder.cdef("""
90
+ /* unused in v1 — Python uses database_create_with_config */
91
+ database_t* database_create(const char* location, size_t lru_memory_mb,
92
+ void* wal_config, uint8_t chunk_size, uint32_t btree_node_size,
93
+ uint8_t enable_persist, void* pool, void* wheel, int* error_code);
94
+ database_t* database_create_with_config(const char* location,
95
+ database_config_t* config, int* error_code);
96
+ database_t* database_create_encrypted(const char* location,
97
+ encrypted_database_config_t* config, int* error_code);
98
+ void database_destroy(database_t* db);
99
+
100
+ database_config_t* database_config_default(void);
101
+ database_config_t* database_config_copy(const database_config_t* config);
102
+ void database_config_destroy(database_config_t* config);
103
+ void database_config_set_chunk_size(database_config_t*, uint8_t);
104
+ void database_config_set_btree_node_size(database_config_t*, uint32_t);
105
+ void database_config_set_enable_persist(database_config_t*, uint8_t);
106
+ void database_config_set_lru_memory_mb(database_config_t*, size_t);
107
+ void database_config_set_lru_shards(database_config_t*, uint16_t);
108
+ void database_config_set_wal_sync_mode(database_config_t*, uint8_t);
109
+ void database_config_set_wal_debounce_ms(database_config_t*, uint64_t);
110
+ void database_config_set_worker_threads(database_config_t*, uint8_t);
111
+ void database_config_set_sync_only(database_config_t*, uint8_t);
112
+
113
+ encrypted_database_config_t* encrypted_database_config_default(void);
114
+ void encrypted_database_config_destroy(encrypted_database_config_t*);
115
+ void encrypted_database_config_set_type(encrypted_database_config_t*, int);
116
+ void encrypted_database_config_set_symmetric_key(encrypted_database_config_t*, const uint8_t*, size_t);
117
+ void encrypted_database_config_set_asymmetric_private_key(encrypted_database_config_t*, const uint8_t*, size_t);
118
+ void encrypted_database_config_set_asymmetric_public_key(encrypted_database_config_t*, const uint8_t*, size_t);
119
+ """)
120
+
121
+ ffibuilder.cdef("""
122
+ int database_put_sync_raw(database_t* db,
123
+ const char* key, size_t key_len, char delimiter,
124
+ const uint8_t* value, size_t value_len);
125
+ int database_get_sync_raw(database_t* db,
126
+ const char* key, size_t key_len, char delimiter,
127
+ uint8_t** value_out, size_t* value_len_out);
128
+ int database_delete_sync_raw(database_t* db,
129
+ const char* key, size_t key_len, char delimiter);
130
+ void database_raw_value_free(uint8_t* value);
131
+ int database_batch_sync_raw(database_t* db, char delimiter,
132
+ const raw_op_t* ops, size_t count);
133
+ int database_scan_sync_raw(database_t* db,
134
+ const char* prefix, size_t prefix_len, char delimiter,
135
+ raw_result_t** results, size_t* count);
136
+ int database_scan_range_sync_raw(database_t* db,
137
+ const char* start_prefix, size_t start_prefix_len,
138
+ const char* end_prefix, size_t end_prefix_len,
139
+ char delimiter,
140
+ raw_result_t** results, size_t* count);
141
+ void database_raw_results_free(raw_result_t* results, size_t count);
142
+ """)
143
+
144
+ ffibuilder.cdef("""
145
+ int database_put_raw(database_t* db,
146
+ const char* key, size_t key_len, char delimiter,
147
+ const uint8_t* value, size_t value_len, promise_t* promise);
148
+ int database_get_raw(database_t* db,
149
+ const char* key, size_t key_len, char delimiter, promise_t* promise);
150
+ int database_delete_raw(database_t* db,
151
+ const char* key, size_t key_len, char delimiter, promise_t* promise);
152
+ int database_batch_raw(database_t* db, char delimiter,
153
+ const raw_op_t* ops, size_t count, promise_t* promise);
154
+ """)
155
+
156
+ ffibuilder.cdef("""
157
+ typedef void (*promise_resolve_cb)(void* ctx, void* payload);
158
+ typedef void (*promise_reject_cb)(void* ctx, async_error_t* error);
159
+ promise_t* promise_create(promise_resolve_cb resolve, promise_reject_cb reject, void* ctx);
160
+ void promise_destroy(promise_t* promise);
161
+ void promise_resolve(promise_t* promise, void* payload);
162
+ void promise_reject(promise_t* promise, async_error_t* error);
163
+ const char* error_get_message(async_error_t* error);
164
+ const char* error_get_file(async_error_t* error);
165
+ const char* error_get_function(async_error_t* error);
166
+ int error_get_line(async_error_t* error);
167
+ void error_destroy(async_error_t* error);
168
+ """)
169
+
170
+ ffibuilder.cdef("""
171
+ /* unused in v1 — Task 10 iterator uses database_scan_range_sync_raw (string-based) */
172
+ database_iterator_t* database_scan_start(database_t* db,
173
+ path_t* start_path, path_t* end_path);
174
+ int database_scan_next(database_iterator_t* iter,
175
+ path_t** out_path, identifier_t** out_value);
176
+ void database_scan_end(database_iterator_t* iter);
177
+ """)
178
+
179
+ ffibuilder.cdef("""
180
+ database_subtree_t* database_subtree_open(database_t* db,
181
+ const char* prefix, char delimiter);
182
+ int database_subtree_delete_prefix(database_t* db,
183
+ const char* prefix, char delimiter);
184
+ void database_subtree_close(database_subtree_t* subtree);
185
+ int database_subtree_put_sync_raw(database_subtree_t* st,
186
+ const char* key, size_t key_len, char delimiter,
187
+ const uint8_t* value, size_t value_len);
188
+ int database_subtree_get_sync_raw(database_subtree_t* st,
189
+ const char* key, size_t key_len, char delimiter,
190
+ uint8_t** value_out, size_t* value_len_out);
191
+ int database_subtree_delete_sync_raw(database_subtree_t* st,
192
+ const char* key, size_t key_len, char delimiter);
193
+ int database_subtree_put_raw(database_subtree_t* st,
194
+ const char* key, size_t key_len, char delimiter,
195
+ const uint8_t* value, size_t value_len, promise_t* promise);
196
+ int database_subtree_get_raw(database_subtree_t* st,
197
+ const char* key, size_t key_len, char delimiter, promise_t* promise);
198
+ int database_subtree_delete_raw(database_subtree_t* st,
199
+ const char* key, size_t key_len, char delimiter, promise_t* promise);
200
+ """)
201
+
202
+ ffibuilder.cdef("""
203
+ graph_layer_t* graph_layer_create(const char* path,
204
+ graph_layer_config_t* config, database_subtree_t* subtree,
205
+ int* error_code);
206
+ void graph_layer_destroy(graph_layer_t* layer);
207
+ int graph_insert_sync(graph_layer_t* layer, const char* s, const char* p, const char* o);
208
+ size_t graph_triple_expand_ops(graph_layer_t* layer,
209
+ const char* s, const char* p, const char* o,
210
+ int type, raw_op_t* out_ops, size_t max_ops);
211
+ graph_query_t* graph_query_create(graph_layer_t* layer);
212
+ void graph_query_destroy(graph_query_t* q);
213
+ int graph_query_vertex(graph_query_t* q, const char* id);
214
+ int graph_query_out(graph_query_t* q, const char* predicate);
215
+ int graph_query_in(graph_query_t* q, const char* predicate);
216
+ int graph_query_has(graph_query_t* q, const char* predicate, const char* value);
217
+ int graph_query_intersect(graph_query_t* q, graph_query_t* left, graph_query_t* right);
218
+ int graph_query_union(graph_query_t* q, graph_query_t* left, graph_query_t* right);
219
+ int graph_query_limit(graph_query_t* q, size_t limit);
220
+ graph_result_t* graph_query_execute_sync(graph_query_t* q);
221
+ size_t graph_result_count(graph_result_t* r);
222
+ const char* const* graph_result_vertices(graph_result_t* r);
223
+ void graph_result_destroy(graph_result_t* r);
224
+ """)
225
+
226
+ ffibuilder.cdef("""
227
+ graphql_layer_t* graphql_layer_create(const char* path,
228
+ const graphql_layer_config_t* config, database_subtree_t* subtree,
229
+ int* error_code);
230
+ void graphql_layer_destroy(graphql_layer_t* layer);
231
+ int graphql_schema_parse(graphql_layer_t* layer, const char* sdl, char** error_out);
232
+ graphql_result_t* graphql_query_sync(graphql_layer_t* layer, const char* query);
233
+ void graphql_result_destroy(graphql_result_t* r);
234
+ const char* graphql_result_to_json(graphql_result_t* result);
235
+ void free(void* ptr);
236
+ """)
237
+
238
+
239
+ def configure(library_dir: str, include_dirs: list[str] | None = None) -> None:
240
+ """Call set_source with build-time parameters.
241
+
242
+ setup.py calls this after the CMake build produces libwavedb.so in
243
+ library_dir. The rpath ensures the compiled extension finds the .so
244
+ at runtime relative to the package directory.
245
+ """
246
+ if sys.platform == "darwin":
247
+ rpath = "@loader_path/_lib"
248
+ elif sys.platform == "win32":
249
+ rpath = None # Windows: no rpath; .dll is in the same dir
250
+ else:
251
+ rpath = "$ORIGIN/_lib"
252
+
253
+ extra_link_args = []
254
+ if rpath:
255
+ extra_link_args.append(f"-Wl,-rpath,{rpath}")
256
+
257
+ kwargs = dict(
258
+ libraries=["wavedb"],
259
+ library_dirs=[library_dir],
260
+ extra_link_args=extra_link_args,
261
+ )
262
+ if include_dirs:
263
+ kwargs["include_dirs"] = include_dirs
264
+
265
+ ffibuilder.set_source(
266
+ "wavedb._native_ext",
267
+ _C_SOURCE,
268
+ **kwargs,
269
+ )
wavedb/_errors.py ADDED
@@ -0,0 +1,49 @@
1
+ """Map C error codes and string markers to typed Python exceptions."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable
5
+
6
+ from .exceptions import (
7
+ EncryptionError,
8
+ IOError_,
9
+ InvalidPathError,
10
+ NotFoundError,
11
+ WaveDBError,
12
+ )
13
+
14
+
15
+ _DATABASE_ERR_ENCRYPTION_REQUIRED = -100
16
+ _DATABASE_ERR_ENCRYPTION_KEY_INVALID = -101
17
+ _DATABASE_ERR_ENCRYPTION_UNSUPPORTED = -102
18
+
19
+
20
+ def map_error(code: int, message: str) -> WaveDBError:
21
+ msg = message or ""
22
+
23
+ if code == _DATABASE_ERR_ENCRYPTION_REQUIRED:
24
+ return EncryptionError(msg or "encryption required")
25
+ if code == _DATABASE_ERR_ENCRYPTION_KEY_INVALID:
26
+ return EncryptionError(msg or "invalid encryption key")
27
+ if code == _DATABASE_ERR_ENCRYPTION_UNSUPPORTED:
28
+ return EncryptionError(msg or "encryption unsupported")
29
+
30
+ # String markers below are a Python-side convention for surfacing error
31
+ # context; the C sync APIs return integer codes, not strings. Markers
32
+ # match what the Node.js C++ layer emits for parity.
33
+ upper = msg.upper()
34
+ if "NOT_FOUND" in upper or "KEY NOT FOUND" in upper:
35
+ return NotFoundError(msg)
36
+ if "INVALID_PATH" in upper or "INVALID PATH" in upper:
37
+ return InvalidPathError(msg)
38
+ if "IO_ERROR" in upper or "I/O ERROR" in upper or "DATABASE_CLOSED" in upper:
39
+ return IOError_(msg)
40
+ if "ENCRYPTION" in upper:
41
+ return EncryptionError(msg)
42
+
43
+ return WaveDBError(msg or f"WaveDB error (code {code})")
44
+
45
+
46
+ def raise_on_error(code: int, message_factory: "Callable[[], str] | str") -> None:
47
+ """If code != 0, raise a mapped exception. message_factory is called only on error."""
48
+ if code != 0:
49
+ raise map_error(code, message_factory() if callable(message_factory) else str(message_factory))
@@ -0,0 +1 @@
1
+ """Directory holding the bundled libwavedb shared library."""
Binary file
wavedb/_lib/wavedb.dll ADDED
Binary file