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.
- arcadedb_embedded_native-26.10.2.dev1.dist-info/METADATA +114 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/RECORD +41 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/WHEEL +5 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/LICENSE +201 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/NOTICE +25 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/THIRD_PARTY_NOTICES.md +3629 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/Apache-2.0.txt +73 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-CE-LICENSE.txt +430 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-THIRD_PARTY_LICENSE.txt +7357 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/licenses/licenses/GraalVM-native-image-LICENSE.txt +379 -0
- arcadedb_embedded_native-26.10.2.dev1.dist-info/top_level.txt +1 -0
- arcadedb_native/__init__.py +128 -0
- arcadedb_native/_build_info.py +5 -0
- arcadedb_native/_jpype_stub.py +44 -0
- arcadedb_native/_logging.py +23 -0
- arcadedb_native/_native/__init__.py +31 -0
- arcadedb_native/_native/bulk.py +96 -0
- arcadedb_native/_native/codec.py +229 -0
- arcadedb_native/_native/columns.py +167 -0
- arcadedb_native/_native/database.py +1356 -0
- arcadedb_native/_native/extras.py +819 -0
- arcadedb_native/_native/lib.py +338 -0
- arcadedb_native/_native/libarcadedb.dll +4 -0
- arcadedb_native/_native/records.py +204 -0
- arcadedb_native/_native/schema_objects.py +379 -0
- arcadedb_native/_native/server.py +118 -0
- arcadedb_native/async_executor.py +1114 -0
- arcadedb_native/core.py +2189 -0
- arcadedb_native/dataframe.py +479 -0
- arcadedb_native/exceptions.py +147 -0
- arcadedb_native/exporter.py +309 -0
- arcadedb_native/graph.py +302 -0
- arcadedb_native/graph_batch.py +527 -0
- arcadedb_native/importer.py +284 -0
- arcadedb_native/jvm.py +632 -0
- arcadedb_native/results.py +1369 -0
- arcadedb_native/schema.py +758 -0
- arcadedb_native/server.py +347 -0
- arcadedb_native/transactions.py +38 -0
- arcadedb_native/type_conversion.py +707 -0
- 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
|
+
)
|