arcadedb-embedded-native 26.10.2.dev1__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.
Files changed (41) hide show
  1. arcadedb_embedded_native-26.10.2.dev1.dist-info/METADATA +114 -0
  2. arcadedb_embedded_native-26.10.2.dev1.dist-info/RECORD +41 -0
  3. arcadedb_embedded_native-26.10.2.dev1.dist-info/WHEEL +5 -0
  4. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/LICENSE +201 -0
  5. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/NOTICE +25 -0
  6. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/THIRD_PARTY_NOTICES.md +3629 -0
  7. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/Apache-2.0.txt +73 -0
  8. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-CE-LICENSE.txt +430 -0
  9. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-THIRD_PARTY_LICENSE.txt +7357 -0
  10. arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-native-image-LICENSE.txt +379 -0
  11. arcadedb_embedded_native-26.10.2.dev1.dist-info/top_level.txt +1 -0
  12. arcadedb_native/__init__.py +128 -0
  13. arcadedb_native/_build_info.py +5 -0
  14. arcadedb_native/_jpype_stub.py +44 -0
  15. arcadedb_native/_logging.py +23 -0
  16. arcadedb_native/_native/__init__.py +31 -0
  17. arcadedb_native/_native/bulk.py +96 -0
  18. arcadedb_native/_native/codec.py +229 -0
  19. arcadedb_native/_native/columns.py +167 -0
  20. arcadedb_native/_native/database.py +1356 -0
  21. arcadedb_native/_native/extras.py +819 -0
  22. arcadedb_native/_native/lib.py +338 -0
  23. arcadedb_native/_native/libarcadedb.dll +4 -0
  24. arcadedb_native/_native/records.py +204 -0
  25. arcadedb_native/_native/schema_objects.py +379 -0
  26. arcadedb_native/_native/server.py +118 -0
  27. arcadedb_native/async_executor.py +1114 -0
  28. arcadedb_native/core.py +2189 -0
  29. arcadedb_native/dataframe.py +479 -0
  30. arcadedb_native/exceptions.py +147 -0
  31. arcadedb_native/exporter.py +309 -0
  32. arcadedb_native/graph.py +302 -0
  33. arcadedb_native/graph_batch.py +527 -0
  34. arcadedb_native/importer.py +284 -0
  35. arcadedb_native/jvm.py +632 -0
  36. arcadedb_native/results.py +1369 -0
  37. arcadedb_native/schema.py +758 -0
  38. arcadedb_native/server.py +347 -0
  39. arcadedb_native/transactions.py +38 -0
  40. arcadedb_native/type_conversion.py +707 -0
  41. arcadedb_native/vector.py +800 -0
@@ -0,0 +1,819 @@
1
+ """Native backend coverage beyond the DSL core: GraphBatch, the async executor, database settings, import/export and Python
2
+ functions in SQL, over the ``arcadedb_n_gb*``, ``arcadedb_n_async``, ``arcadedb_n_dbset``, ``arcadedb_n_import``,
3
+ ``arcadedb_n_export`` and ``arcadedb_n_udf_register`` entry points (``NativeExtras.java``).
4
+
5
+ Callbacks (async completions, error handlers, Python functions called from SQL) go through ONE ctypes function pointer that
6
+ ``_ensure_upcall`` registers with the library: the engine thread (or an async writer thread) calls ``_dispatch(id, json)``,
7
+ ctypes takes the GIL for the call, and the reply is JSON (see the header of ``NativeExtras.java`` for GIL and exception rules).
8
+ """
9
+
10
+ import ctypes
11
+ import itertools
12
+ import json
13
+ import threading
14
+ from collections.abc import Mapping
15
+ from typing import Any, Callable, Iterable, Optional
16
+
17
+ from ..exceptions import ArcadeDBError
18
+ from ..graph_batch import GraphBatch as _BaseGraphBatch
19
+ from . import lib, not_yet
20
+ from .codec import _PLAIN, _default, _prep, loads
21
+
22
+ # -------------------------------------------------------------------------------------------------------- upcall
23
+
24
+ _UPCALL_TYPE = ctypes.CFUNCTYPE(
25
+ ctypes.c_void_p, ctypes.c_longlong, ctypes.c_char_p
26
+ ) # (Java long id, char *json)
27
+ _callbacks: dict = {}
28
+ _callback_ids = itertools.count(1)
29
+ _callbacks_lock = threading.Lock()
30
+ _reply = threading.local()
31
+ _upcall_ref = None
32
+
33
+
34
+ def _enc(value) -> str:
35
+ """JSON text of a value with the tagged encoding of query parameters."""
36
+ return json.dumps(_prep(value), default=_default)
37
+
38
+
39
+ def _dispatch(cid, raw):
40
+ try:
41
+ fn = _callbacks.get(cid)
42
+ message = loads(raw.decode("utf-8")) if raw else {}
43
+ reply = fn(message) if fn is not None else None
44
+ data = None if reply is None else _enc(reply).encode("utf-8")
45
+ except BaseException as exc: # nosec B110 - an exception must not cross the C frame
46
+ data = json.dumps({"error": f"{type(exc).__name__}: {exc}"}).encode("utf-8")
47
+ if data is None:
48
+ return None
49
+ # kept alive until this thread's next callback: Java copies the text before it returns
50
+ buf = ctypes.create_string_buffer(data)
51
+ _reply.buf = buf
52
+ return ctypes.addressof(buf)
53
+
54
+
55
+ def _ensure_upcall() -> None:
56
+ global _upcall_ref
57
+ if _upcall_ref is not None:
58
+ return
59
+ lib.call_void(
60
+ "arcadedb_n_set_upcall", ctypes.cast(_make_pointer(), ctypes.c_void_p)
61
+ )
62
+
63
+
64
+ def _make_pointer():
65
+ global _upcall_ref
66
+ _upcall_ref = _UPCALL_TYPE(_dispatch)
67
+ return _upcall_ref
68
+
69
+
70
+ def _register(fn: Callable) -> int:
71
+ _ensure_upcall()
72
+ with _callbacks_lock:
73
+ cid = next(_callback_ids)
74
+ _callbacks[cid] = fn
75
+ return cid
76
+
77
+
78
+ def _error_from(message: dict) -> ArcadeDBError:
79
+ reply = message.get("err") or {}
80
+ exc = ArcadeDBError(f"{reply.get('cls')}: {reply.get('error')}")
81
+ exc.native = reply
82
+ exc.java_chain = reply.get("chain") or []
83
+ return exc
84
+
85
+
86
+ def _rid_of(value) -> str:
87
+ """The RID text of a record wrapper, a RID or its text."""
88
+ get_rid = getattr(value, "get_rid", None)
89
+ return get_rid() if get_rid is not None else str(value)
90
+
91
+
92
+ def _call(prefix: str, name: str, *args) -> dict:
93
+ from .database import _call as call
94
+
95
+ return call(prefix, name, *args)
96
+
97
+
98
+ # -------------------------------------------------------------------------------------------------------- GraphBatch
99
+
100
+
101
+ class NativeGraphBatch(_BaseGraphBatch):
102
+ """``GraphBatch`` over the engine's batch builder in the image (same names and behaviour as the JPype wrapper)."""
103
+
104
+ _BULK_CHUNK = 10_000 # rows per crossing: small enough that a chunk's text, rows and reply die in the young generation
105
+
106
+ def __init__(self, database, handle: int):
107
+ self._db = database
108
+ self._gb = handle
109
+ self._closed = False
110
+
111
+ @classmethod
112
+ def create(cls, database, **options) -> "NativeGraphBatch":
113
+ wal_flush = options.get("wal_flush")
114
+ if wal_flush is not None:
115
+ options["wal_flush"] = cls._to_java_wal_flush_name(wal_flush)
116
+ given = {k: v for k, v in options.items() if v is not None}
117
+ try:
118
+ reply = _call(
119
+ "Failed to create GraphBatch",
120
+ "arcadedb_n_gb_create",
121
+ database._h,
122
+ lib.b(_enc(given)),
123
+ )
124
+ except ArcadeDBError:
125
+ raise
126
+ return cls(database, reply["h"])
127
+
128
+ @classmethod
129
+ def _to_java_wal_flush_name(cls, mode: str) -> str:
130
+ normalized = mode.lower()
131
+ if normalized not in cls._VALID_WAL_FLUSH_MODES:
132
+ raise ValueError(
133
+ f"Invalid wal_flush mode: {mode}. Must be one of: {list(cls._VALID_WAL_FLUSH_MODES.keys())}"
134
+ )
135
+ return cls._VALID_WAL_FLUSH_MODES[normalized]
136
+
137
+ def _op(self, prefix: str, op: str, **args) -> dict:
138
+ return _call(prefix, "arcadedb_n_gb", self._gb, lib.b(op), lib.b(_enc(args)))
139
+
140
+ def new_vertex(self, type_name: str):
141
+ from .records import Vertex
142
+
143
+ self._check_not_closed()
144
+ return Vertex(self._db, type_name)
145
+
146
+ def create_vertex(self, type_name: str, **properties):
147
+ from .records import Vertex
148
+
149
+ self._check_not_closed()
150
+ db = self._db
151
+ started = False
152
+ try:
153
+ if not db.is_transaction_active():
154
+ started = True
155
+ db.begin()
156
+ reply = self._op(
157
+ f"Failed to create batch vertex of type '{type_name}'",
158
+ "create_vertex",
159
+ type=type_name,
160
+ props=self._to_java_varargs(properties),
161
+ )
162
+ if started:
163
+ db.commit()
164
+ return Vertex(db, type_name, reply["rid"], properties)
165
+ except BaseException as e:
166
+ if started:
167
+ try:
168
+ if db.is_transaction_active():
169
+ db.rollback()
170
+ except (
171
+ Exception
172
+ ): # nosec B110 - the original error is the one to report
173
+ pass
174
+ if isinstance(e, ArcadeDBError) or not isinstance(e, Exception):
175
+ raise
176
+ raise ArcadeDBError(
177
+ f"Failed to create batch vertex of type '{type_name}': {e}"
178
+ ) from e
179
+
180
+ def _to_java_varargs(self, properties: dict) -> dict:
181
+ """The properties of one vertex as the library takes them (the step the JPype path converts to Java values in)."""
182
+ if not isinstance(properties, dict):
183
+ raise TypeError("vertex properties must be a dict")
184
+ for key in properties:
185
+ if type(key) is not str:
186
+ raise TypeError(f"property names must be str, got {key!r}")
187
+ return properties
188
+
189
+ def _create_vertices_json_bulk(self, type_name: str, rows):
190
+ """The bulk path (JSON rows in, RIDs out): the RIDs, or None for rows JSON would change (the caller falls back)."""
191
+ from ..type_conversion import json_bulk_scalar_ok
192
+
193
+ for row in rows:
194
+ for key, value in (row or {}).items():
195
+ if type(key) is not str or not json_bulk_scalar_ok(value):
196
+ return None
197
+ return self._op(
198
+ f"Failed to create batch vertices for type '{type_name}'",
199
+ "create_vertices",
200
+ type=type_name,
201
+ rows=[r or {} for r in rows],
202
+ )["rids"]
203
+
204
+ def create_vertices(self, type_name: str, count_or_properties) -> list:
205
+ self._check_not_closed()
206
+ db = self._db
207
+ owns_transaction = not db.is_transaction_active()
208
+ prefix = f"Failed to create batch vertices for type '{type_name}'"
209
+ try:
210
+ chunk = self._BULK_CHUNK
211
+ if isinstance(count_or_properties, int):
212
+ rids = []
213
+ for start in range(0, count_or_properties, chunk):
214
+ rids.extend(
215
+ self._op(
216
+ prefix,
217
+ "create_vertices",
218
+ type=type_name,
219
+ count=min(chunk, count_or_properties - start),
220
+ )["rids"]
221
+ )
222
+ if not count_or_properties:
223
+ rids = self._op(prefix, "create_vertices", type=type_name, count=0)[
224
+ "rids"
225
+ ]
226
+ return rids
227
+ rows = list(count_or_properties)
228
+ for row in rows:
229
+ if row is not None and not isinstance(row, dict):
230
+ raise TypeError(
231
+ "create_vertices() property rows must be dicts or None"
232
+ )
233
+ # chunked, as the JPype wrapper does: one crossing, one JSON text and one reply per chunk, so a 1M-vertex load
234
+ # never holds the text, the parsed rows and the reply of all of it at once (the Serial GC copies what lives)
235
+ rids = []
236
+ for start in range(0, len(rows), chunk):
237
+ rids.extend(
238
+ self._op(
239
+ prefix,
240
+ "create_vertices",
241
+ type=type_name,
242
+ rows=[r or {} for r in rows[start : start + chunk]],
243
+ )["rids"]
244
+ )
245
+ if not rows:
246
+ rids = self._op(prefix, "create_vertices", type=type_name, rows=[])[
247
+ "rids"
248
+ ]
249
+ return rids
250
+ except BaseException as e:
251
+ if owns_transaction:
252
+ try:
253
+ if db.is_transaction_active():
254
+ db.rollback()
255
+ except Exception: # nosec B110
256
+ pass
257
+ if isinstance(e, (TypeError, ArcadeDBError)) or not isinstance(
258
+ e, Exception
259
+ ):
260
+ raise
261
+ raise ArcadeDBError(f"{prefix}: {e}") from e
262
+
263
+ @staticmethod
264
+ def _rid_text(value) -> str:
265
+ rid = getattr(value, "get_rid", None)
266
+ if rid is not None:
267
+ return rid()
268
+ return str(value)
269
+
270
+ def new_edge(
271
+ self,
272
+ source_vertex_or_rid,
273
+ edge_type: str,
274
+ destination_vertex_or_rid,
275
+ **properties,
276
+ ):
277
+ self._check_not_closed()
278
+ self._op(
279
+ f"Failed to buffer batch edge '{edge_type}'",
280
+ "new_edge",
281
+ src=self._rid_text(source_vertex_or_rid),
282
+ type=edge_type,
283
+ dst=self._rid_text(destination_vertex_or_rid),
284
+ props=properties,
285
+ )
286
+ return self
287
+
288
+ def new_edges(self, source_rids, edge_type: str, destination_rids, properties=None):
289
+ self._check_not_closed()
290
+ source_rids = [self._rid_text(r) for r in source_rids]
291
+ destination_rids = [self._rid_text(r) for r in destination_rids]
292
+ if properties is not None:
293
+ properties = list(properties)
294
+ lengths = [len(source_rids), len(destination_rids)]
295
+ if properties is not None:
296
+ lengths.append(len(properties))
297
+ if len(set(lengths)) != 1:
298
+ names = ["source_rids", "destination_rids", "properties"]
299
+ got = ", ".join(f"{n} {k}" for n, k in zip(names, lengths))
300
+ raise ArcadeDBError(
301
+ f"Failed to buffer batch edges '{edge_type}': source_rids, destination_rids and properties must have "
302
+ f"the same length ({got})"
303
+ )
304
+ for p in properties or ():
305
+ for key in p or ():
306
+ if not isinstance(key, str):
307
+ raise ArcadeDBError(
308
+ f"Failed to buffer batch edges '{edge_type}': property names must be str, got {key!r}"
309
+ )
310
+ chunk = self._BULK_CHUNK
311
+ if properties is None:
312
+ # no properties: the RIDs of a chunk go over as two ';'-joined texts (as EdgeBatcher.newEdgesJoined takes them on the
313
+ # JPype path) and are read straight into RIDs, not as two JSON lists of Strings
314
+ for start in range(0, len(source_rids), chunk):
315
+ self._op(
316
+ f"Failed to buffer batch edges '{edge_type}'",
317
+ "new_edges",
318
+ src=";".join(source_rids[start : start + chunk]),
319
+ type=edge_type,
320
+ dst=";".join(destination_rids[start : start + chunk]),
321
+ props=None,
322
+ )
323
+ return self
324
+ for start in range(0, len(source_rids), chunk):
325
+ self._op(
326
+ f"Failed to buffer batch edges '{edge_type}'",
327
+ "new_edges",
328
+ src=source_rids[start : start + chunk],
329
+ type=edge_type,
330
+ dst=destination_rids[start : start + chunk],
331
+ props=(
332
+ None
333
+ if properties is None
334
+ else [p or {} for p in properties[start : start + chunk]]
335
+ ),
336
+ )
337
+ return self
338
+
339
+ def flush(self):
340
+ self._check_not_closed()
341
+ self._op("Failed to flush GraphBatch", "flush")
342
+ return self
343
+
344
+ def close(self):
345
+ if self._closed:
346
+ return
347
+ refused = False
348
+ try:
349
+ self._op("Failed to close GraphBatch", "close")
350
+ except ArcadeDBError as e:
351
+ # an IllegalStateException while the caller has a transaction open: the batch is still open with its work pending
352
+ refused = (getattr(e, "native", None) or {}).get(
353
+ "cls"
354
+ ) == "java.lang.IllegalStateException" and bool(
355
+ self._db.is_transaction_active()
356
+ )
357
+ raise
358
+ finally:
359
+ if not refused:
360
+ self._closed = True
361
+
362
+ def get_total_edges_created(self) -> int:
363
+ self._check_not_closed()
364
+ return int(self._op("Failed to read GraphBatch", "stats")["total_edges"])
365
+
366
+ def get_buffered_edge_count(self) -> int:
367
+ self._check_not_closed()
368
+ return int(self._op("Failed to read GraphBatch", "stats")["buffered"])
369
+
370
+ def get_deferred_incoming_edge_count(self) -> int:
371
+ self._check_not_closed()
372
+ return int(self._op("Failed to read GraphBatch", "stats")["deferred"])
373
+
374
+
375
+ # -------------------------------------------------------------------------------------------------------- async
376
+
377
+
378
+ class NativeAsyncExecutor:
379
+ """The database's async executor (one per database) with the JPype wrapper's names; callbacks run on the engine's writer
380
+ threads and take the GIL, as the JPype proxies do."""
381
+
382
+ def __init__(self, database):
383
+ self._db = database
384
+ self._closed = False
385
+ self._owner = database
386
+ self._cb_ids: list = []
387
+
388
+ def __enter__(self):
389
+ return self
390
+
391
+ def __exit__(self, exc_type, exc_val, exc_tb):
392
+ self.close()
393
+
394
+ def is_closed(self) -> bool:
395
+ return self._closed
396
+
397
+ def _op(self, op: str, **args) -> dict:
398
+ return _call(
399
+ "Async executor failed",
400
+ "arcadedb_n_async",
401
+ self._db._h,
402
+ lib.b(op),
403
+ lib.b(_enc(args)),
404
+ )
405
+
406
+ def _settings(self) -> dict:
407
+ return self._op("get")
408
+
409
+ def _set(self, key, value):
410
+ self._op("set", key=key, value=value)
411
+ return self
412
+
413
+ # configuration
414
+ def set_parallel_level(self, level: int):
415
+ if level < 1:
416
+ raise ValueError("parallel_level must be at least 1")
417
+ return self._set("parallel", level)
418
+
419
+ def set_commit_every(self, count: int):
420
+ if count < 1:
421
+ raise ValueError(
422
+ f"commit_every must be >= 1 (got {count}); the async executor always commits in batches"
423
+ )
424
+ return self._set("commit_every", count)
425
+
426
+ def set_transaction_use_wal(self, use_wal: bool):
427
+ return self._set("wal", bool(use_wal))
428
+
429
+ def set_transaction_sync(self, sync_mode: str):
430
+ valid = {"no": "NO", "yes_nometadata": "YES_NOMETADATA", "yes_full": "YES_FULL"}
431
+ if sync_mode not in valid:
432
+ raise ValueError(f"sync_mode must be one of {set(valid)}, got {sync_mode}")
433
+ return self._set("sync", valid[sync_mode])
434
+
435
+ def set_back_pressure(self, percentage: int):
436
+ if percentage < 0 or percentage > 100:
437
+ raise ValueError("back_pressure must be between 0 and 100")
438
+ return self._set("back_pressure", percentage)
439
+
440
+ def get_parallel_level(self) -> int:
441
+ return int(self._settings()["parallel"])
442
+
443
+ def get_back_pressure(self) -> int:
444
+ return int(self._settings()["back_pressure"])
445
+
446
+ def get_commit_every(self) -> int:
447
+ return int(self._settings()["commit_every"])
448
+
449
+ def is_transaction_use_wal(self) -> bool:
450
+ return bool(self._settings()["wal"])
451
+
452
+ def get_transaction_sync(self) -> str:
453
+ return self._settings()["sync"].lower()
454
+
455
+ def get_thread_count(self) -> int:
456
+ return int(self._settings()["threads"])
457
+
458
+ def is_processing(self) -> bool:
459
+ return bool(self._settings()["processing"])
460
+
461
+ def is_pending(self) -> bool:
462
+ return self.is_processing()
463
+
464
+ def kill(self):
465
+ self._op("kill")
466
+
467
+ # work
468
+ def _record_callback(self, callback, kind="d"):
469
+ from .records import _wrap_dict
470
+
471
+ def handler(message):
472
+ callback(_wrap_dict(self._db, message["rec"], kind))
473
+
474
+ return _register(handler)
475
+
476
+ def _error_callback(self, callback):
477
+ return _register(lambda message: callback(_error_from(message)))
478
+
479
+ def create_record(self, document, callback=None, error_callback=None):
480
+ kind = "v" if hasattr(document, "get_out_edges") else "d"
481
+ cb = self._record_callback(callback, kind) if callback is not None else 0
482
+ err = self._error_callback(error_callback) if error_callback is not None else 0
483
+ self._op(
484
+ "create_record",
485
+ type=document._type,
486
+ kind=kind,
487
+ props=document._props,
488
+ cb=cb,
489
+ err=err,
490
+ )
491
+
492
+ def _statement(self, op, language, text, callback, args, params, error_callback):
493
+ from .database import NativeResultSet
494
+
495
+ args = tuple(args or ())
496
+ if args and params:
497
+ raise ValueError("Use either positional args or named params, not both")
498
+ cb = 0
499
+ if callback is not None:
500
+
501
+ def on_result(message):
502
+ # the JPype wrapper calls the callback once per row of the completed statement
503
+ for row in NativeResultSet(self._db, message["reply"]):
504
+ callback(row)
505
+
506
+ cb = _register(on_result)
507
+ err = self._error_callback(error_callback) if error_callback is not None else 0
508
+ if params:
509
+ values = dict(params)
510
+ elif args:
511
+ values = (
512
+ dict(args[0])
513
+ if len(args) == 1 and isinstance(args[0], Mapping)
514
+ else list(args)
515
+ )
516
+ else:
517
+ values = None
518
+ self._op(op, lang=language, text=text, params=values, cb=cb, err=err)
519
+
520
+ def query(
521
+ self, language, query_text, callback, args=None, error_callback=None, **params
522
+ ):
523
+ self._statement(
524
+ "query", language, query_text, callback, args, params, error_callback
525
+ )
526
+
527
+ def command(
528
+ self,
529
+ language,
530
+ command_text,
531
+ callback=None,
532
+ args=None,
533
+ error_callback=None,
534
+ **params,
535
+ ):
536
+ self._statement(
537
+ "command", language, command_text, callback, args, params, error_callback
538
+ )
539
+
540
+ def append_samples(
541
+ self, type_name, timestamps, *column_values, primitive: bool = False
542
+ ):
543
+ def plain(values):
544
+ kind = getattr(getattr(values, "dtype", None), "kind", None)
545
+ if (
546
+ kind == "b"
547
+ ): # a numpy bool array is a 0/1 numeric column (a list of bools is not: the engine refuses it)
548
+ values = values.astype("int64")
549
+ return values.tolist() if hasattr(values, "tolist") else list(values)
550
+
551
+ stamps = [int(t) for t in plain(timestamps)]
552
+ if primitive:
553
+ # the engine's primitive TimeSeriesBatch is a JVM-boundary optimisation (no boxing per value); here the rows cross
554
+ # as one JSON text either way, so the columns take the same path, with the primitive path's length check
555
+ for index, values in enumerate(column_values):
556
+ if len(values) != len(stamps):
557
+ raise ValueError(
558
+ f"column {index} has {len(values)} values for {len(stamps)} timestamps"
559
+ )
560
+ self._op(
561
+ "append_samples",
562
+ type=type_name,
563
+ ts=stamps,
564
+ cols=[plain(c) for c in column_values],
565
+ )
566
+
567
+ def _edge_callback(self, callback):
568
+ """The handler of an edge-created event: ``callback(edge, created_source, created_destination)``."""
569
+ from .records import _wrap_dict
570
+
571
+ def handler(message):
572
+ callback(
573
+ _wrap_dict(self._db, message["edge"], "e"),
574
+ bool(message["src"]),
575
+ bool(message["dst"]),
576
+ )
577
+
578
+ return _register(handler)
579
+
580
+ def new_edge(
581
+ self,
582
+ source_vertex,
583
+ edge_type: str,
584
+ destination_vertex_or_rid,
585
+ light: bool = False,
586
+ callback=None,
587
+ **properties,
588
+ ):
589
+ cb = self._edge_callback(callback) if callback is not None else 0
590
+ self._op(
591
+ "new_edge",
592
+ src=_rid_of(source_vertex),
593
+ type=edge_type,
594
+ dst=_rid_of(destination_vertex_or_rid),
595
+ light=bool(light),
596
+ props=properties,
597
+ cb=cb,
598
+ )
599
+
600
+ def new_edge_by_keys(
601
+ self,
602
+ source_vertex_type: str,
603
+ source_key_names,
604
+ source_key_values,
605
+ destination_vertex_type: str,
606
+ destination_key_names,
607
+ destination_key_values,
608
+ create_vertex_if_not_exist: bool,
609
+ edge_type: str,
610
+ bidirectional: bool,
611
+ light: bool,
612
+ callback=None,
613
+ **properties,
614
+ ):
615
+ def listed(names, values, what):
616
+ if isinstance(names, str):
617
+ names = [names]
618
+ values = list(values) if isinstance(values, (list, tuple)) else [values]
619
+ names, values = list(names), list(values)
620
+ if len(names) != len(values):
621
+ raise ValueError(
622
+ f"{what}_key_names and {what}_key_values must have same size"
623
+ )
624
+ return names, values
625
+
626
+ src_names, src_values = listed(source_key_names, source_key_values, "source")
627
+ dst_names, dst_values = listed(
628
+ destination_key_names, destination_key_values, "destination"
629
+ )
630
+ cb = self._edge_callback(callback) if callback is not None else 0
631
+ self._op(
632
+ "new_edge_by_keys",
633
+ src_type=source_vertex_type,
634
+ src_names=src_names,
635
+ src_values=src_values,
636
+ dst_type=destination_vertex_type,
637
+ dst_names=dst_names,
638
+ dst_values=dst_values,
639
+ create=bool(create_vertex_if_not_exist),
640
+ type=edge_type,
641
+ bidirectional=bool(bidirectional),
642
+ light=bool(light),
643
+ props=properties,
644
+ cb=cb,
645
+ )
646
+
647
+ def scan_type(
648
+ self,
649
+ type_name: str,
650
+ callback,
651
+ polymorphic: bool = True,
652
+ error_callback=None,
653
+ ):
654
+ """Scan every record of a type on the writer threads: ``callback(record) -> bool`` (False stops the scan; None goes on)."""
655
+ from .records import _wrap_dict
656
+
657
+ def on_record(message):
658
+ result = callback(_wrap_dict(self._db, message["rec"], "d"))
659
+ return {"v": True if result is None else bool(result)}
660
+
661
+ cb = _register(on_record)
662
+ err = 0
663
+ if error_callback is not None:
664
+
665
+ def on_error(message):
666
+ result = error_callback(
667
+ message.get("rid"), _error_from({"err": message.get("err")})
668
+ )
669
+ return {"v": True if result is None else bool(result)}
670
+
671
+ err = _register(on_error)
672
+ self._op(
673
+ "scan_type", type=type_name, polymorphic=bool(polymorphic), cb=cb, err=err
674
+ )
675
+
676
+ def transaction(self, *args, **kwargs):
677
+ not_yet("AsyncExecutor.transaction")
678
+
679
+ def wait_completion(self, timeout_ms: Optional[int] = None):
680
+ if timeout_ms is None:
681
+ self._op("wait")
682
+ elif timeout_ms < 0:
683
+ raise ValueError(f"timeout_ms must be None or >= 0, got {timeout_ms}")
684
+ elif timeout_ms == 0:
685
+ if self.is_processing():
686
+ raise TimeoutError("Async operations have not completed (0ms poll)")
687
+ elif not self._op("wait", timeout_ms=timeout_ms)["done"]:
688
+ raise TimeoutError(
689
+ f"Async operations did not complete within {timeout_ms}ms"
690
+ )
691
+
692
+ def on_ok(self, callback: Callable[[], None]):
693
+ self._op("on_ok", cb=_register(lambda message: callback()))
694
+ return self
695
+
696
+ def on_error(self, callback: Callable[[Exception], None]):
697
+ self._op("on_error", cb=self._error_callback(callback))
698
+ return self
699
+
700
+ def close(self):
701
+ if self._closed:
702
+ return
703
+ try:
704
+ self._op("close")
705
+ finally:
706
+ self._closed = True
707
+ owner, self._owner = self._owner, None
708
+ if owner is not None:
709
+ owner._discard_async_executor(self)
710
+
711
+
712
+ def insert_parallel(database, type_name: str, rows) -> dict:
713
+ """Hand ``rows`` (a list of dicts) to the async writers; returns the failure token for :func:`failures`."""
714
+ return _call(
715
+ "Failed to bulk-insert",
716
+ "arcadedb_n_async",
717
+ database._h,
718
+ b"insert_parallel",
719
+ lib.b(_enc({"type": type_name, "rows": rows})),
720
+ )
721
+
722
+
723
+ def failures(database, token: int) -> dict:
724
+ return _call(
725
+ "Failed to read the parallel writers",
726
+ "arcadedb_n_async",
727
+ database._h,
728
+ b"failures",
729
+ lib.b(_enc({"fid": token})),
730
+ )
731
+
732
+
733
+ # -------------------------------------------------------------------------------------------------------- settings
734
+
735
+
736
+ def db_set(database, op: str, value: str = "") -> dict:
737
+ return _call(
738
+ f"Failed to set {op}", "arcadedb_n_dbset", database._h, lib.b(op), lib.b(value)
739
+ )
740
+
741
+
742
+ # -------------------------------------------------------------------------------------------------------- import / export
743
+
744
+
745
+ def import_documents(database, settings: dict) -> Optional[dict]:
746
+ from ..importer import _stringify_import_setting
747
+
748
+ text = json.dumps({k: _stringify_import_setting(v) for k, v in settings.items()})
749
+ return _call("Failed to import", "arcadedb_n_import", database._h, lib.b(text))[
750
+ "stats"
751
+ ]
752
+
753
+
754
+ def run_import(database, settings: dict, parallel, commit_every, wal) -> Optional[dict]:
755
+ """The importer with the async executor's settings overridden for the run and restored after it (as the JPype path does)."""
756
+ executor = database._async()
757
+ saved = executor._settings()
758
+ saved_ryw = database.is_read_your_writes()
759
+ try:
760
+ database.set_read_your_writes(False)
761
+ if parallel is not None:
762
+ executor.set_parallel_level(parallel)
763
+ if commit_every is not None:
764
+ executor.set_commit_every(commit_every)
765
+ if wal is not None:
766
+ executor.set_transaction_use_wal(wal)
767
+ return import_documents(database, settings)
768
+ finally:
769
+ try:
770
+ executor.wait_completion()
771
+ finally:
772
+ database.set_read_your_writes(saved_ryw)
773
+ executor._set("parallel", saved["parallel"])
774
+ executor._set("commit_every", saved["commit_every"])
775
+ executor._set("wal", saved["wal"])
776
+
777
+
778
+ def export_database(
779
+ database, path: str, fmt: str, overwrite: bool, settings: dict
780
+ ) -> dict:
781
+ return _call(
782
+ "Database export failed",
783
+ "arcadedb_n_export",
784
+ database._h,
785
+ lib.b(path),
786
+ lib.b(fmt),
787
+ 1 if overwrite else 0,
788
+ lib.b(json.dumps(settings)),
789
+ )["stats"]
790
+
791
+
792
+ # -------------------------------------------------------------------------------------------------------- Python functions in SQL
793
+
794
+
795
+ def register_function(
796
+ name: str, fn: Callable, min_args: int = 0, max_args: int = -1
797
+ ) -> None:
798
+ """Make ``fn`` callable from SQL as ``name(..)``: ``SELECT name(a, b) FROM T``.
799
+
800
+ ``fn`` receives the argument values as Python values and returns a value of a type a parameter can carry (str, int, float,
801
+ bool, None, list, dict, Decimal, datetime). An exception raised by ``fn`` fails the statement with its text. Registration is
802
+ process-wide (the engine's function factory), like any engine function; ``max_args`` < 0 means any number.
803
+ """
804
+
805
+ def handler(message):
806
+ try:
807
+ return {"v": fn(*message.get("args", []))}
808
+ except Exception as exc:
809
+ return {"error": f"{type(exc).__name__}: {exc}"}
810
+
811
+ cid = _register(handler)
812
+ _call(
813
+ "Failed to register the function",
814
+ "arcadedb_n_udf_register",
815
+ lib.b(name),
816
+ cid,
817
+ int(min_args),
818
+ int(max_args if max_args >= 0 else 2**31 - 1),
819
+ )