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,338 @@
|
|
|
1
|
+
"""ctypes loader and call layer for ``libarcadedb`` (native-image shared library)."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..exceptions import ArcadeDBError
|
|
11
|
+
|
|
12
|
+
_LIB_NAMES = {
|
|
13
|
+
"linux": "libarcadedb.so",
|
|
14
|
+
"darwin": "libarcadedb.dylib",
|
|
15
|
+
"win32": "libarcadedb.dll",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# ARCADEDB_NATIVE_BINARY=0 turns off the float32 side buffer for numpy float parameters (phase 6: on by default when the library has it)
|
|
19
|
+
BINARY_PARAMS = os.environ.get("ARCADEDB_NATIVE_BINARY", "1") != "0"
|
|
20
|
+
|
|
21
|
+
# phase 8 switches (each defaults on when the library has the entry point; 0 turns one off, for A/B runs in one image):
|
|
22
|
+
# ARCADEDB_NATIVE_CMD: commands go through arcadedb_n_cmd (cursor id back as a long: no reply text, no free, no JSON decode)
|
|
23
|
+
# ARCADEDB_NATIVE_CLOSE_BATCH: result sets closed by Python are closed 64 at a time in one crossing (0: one crossing each)
|
|
24
|
+
# ARCADEDB_NATIVE_FASTENC: positional parameters of plain scalars are encoded by the cached json encoder, no default hook
|
|
25
|
+
CMD_PATH = os.environ.get("ARCADEDB_NATIVE_CMD", "1") != "0"
|
|
26
|
+
CLOSE_BATCH = int(os.environ.get("ARCADEDB_NATIVE_CLOSE_BATCH", "64"))
|
|
27
|
+
FAST_ENCODE = os.environ.get("ARCADEDB_NATIVE_FASTENC", "1") != "0"
|
|
28
|
+
INSERT_ROWS = os.environ.get("ARCADEDB_NATIVE_INSERT_ROWS", "1") != "0"
|
|
29
|
+
# ARCADEDB_NATIVE_EXEC_MANY: the rows of an insert_many chunk that JSON text cannot carry unchanged go in one crossing (arcadedb_n_exec_many), not one call each
|
|
30
|
+
EXEC_MANY = os.environ.get("ARCADEDB_NATIVE_EXEC_MANY", "1") != "0"
|
|
31
|
+
|
|
32
|
+
_lock = threading.Lock()
|
|
33
|
+
_lib = None
|
|
34
|
+
_isolate = None
|
|
35
|
+
_main_thread = None
|
|
36
|
+
_creator = None
|
|
37
|
+
_local = threading.local()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def library_path() -> Path:
|
|
41
|
+
"""``ARCADEDB_NATIVE_LIB`` if set, else the library next to this module."""
|
|
42
|
+
env = os.environ.get("ARCADEDB_NATIVE_LIB")
|
|
43
|
+
if env:
|
|
44
|
+
return Path(env)
|
|
45
|
+
name = _LIB_NAMES.get(sys.platform, "libarcadedb.so")
|
|
46
|
+
return Path(__file__).with_name(name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _Thread:
|
|
50
|
+
"""The isolate thread of one Python thread; detached when the Python thread ends."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, handle, owned):
|
|
53
|
+
self.handle = handle
|
|
54
|
+
self.owned = owned
|
|
55
|
+
|
|
56
|
+
def __del__(self):
|
|
57
|
+
if self.owned and _lib is not None:
|
|
58
|
+
try:
|
|
59
|
+
_lib.graal_detach_thread(self.handle)
|
|
60
|
+
except Exception: # nosec B110 - interpreter shutdown
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_cpu() -> None:
|
|
65
|
+
"""An image built for x86-64-v3 (AVX2, BMI2, FMA) needs them: fail with a clear error instead of SIGILL on an older CPU."""
|
|
66
|
+
if sys.platform != "linux" or os.environ.get("ARCADEDB_NATIVE_SKIP_CPU_CHECK"):
|
|
67
|
+
return
|
|
68
|
+
try: # the arcadedb-embedded-native wheel records its image's -march; a source checkout uses a locally built (default v3) image
|
|
69
|
+
from .._build_info import NATIVE_MARCH
|
|
70
|
+
except ImportError:
|
|
71
|
+
NATIVE_MARCH = "x86-64-v3"
|
|
72
|
+
if NATIVE_MARCH == "compatibility":
|
|
73
|
+
return # baseline x86-64: runs on every x86-64 CPU
|
|
74
|
+
import platform
|
|
75
|
+
|
|
76
|
+
if platform.machine() not in ("x86_64", "AMD64"):
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
with open("/proc/cpuinfo") as f:
|
|
80
|
+
flags = next((ln for ln in f if ln.startswith("flags")), "").split()
|
|
81
|
+
except OSError:
|
|
82
|
+
return
|
|
83
|
+
missing = [x for x in ("avx2", "bmi2", "fma") if flags and x not in flags]
|
|
84
|
+
if missing:
|
|
85
|
+
raise ArcadeDBError(
|
|
86
|
+
f"The native library needs an x86-64-v3 CPU (AVX2, BMI2, FMA); this CPU lacks {', '.join(missing)}. "
|
|
87
|
+
"Use the arcadedb-embedded (JVM) package instead, or set ARCADEDB_NATIVE_SKIP_CPU_CHECK=1 at your own risk."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _load():
|
|
92
|
+
global _lib, _isolate, _main_thread, _creator
|
|
93
|
+
with _lock:
|
|
94
|
+
if _lib is not None:
|
|
95
|
+
return _lib
|
|
96
|
+
path = library_path()
|
|
97
|
+
if not path.exists():
|
|
98
|
+
raise ArcadeDBError(
|
|
99
|
+
f"Native library not found at {path}. Build it from the native-spike workflow or set "
|
|
100
|
+
"ARCADEDB_NATIVE_LIB."
|
|
101
|
+
)
|
|
102
|
+
_check_cpu()
|
|
103
|
+
# jvector's Highway library (libjvector-<arch>.so) is looked for next to the engine library
|
|
104
|
+
os.environ.setdefault("ARCADEDB_NATIVE_LIBDIR", str(path.resolve().parent))
|
|
105
|
+
lib = ctypes.CDLL(str(path))
|
|
106
|
+
isolate = ctypes.c_void_p()
|
|
107
|
+
thread = ctypes.c_void_p()
|
|
108
|
+
lib.graal_create_isolate.argtypes = [
|
|
109
|
+
ctypes.c_void_p,
|
|
110
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
111
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
112
|
+
]
|
|
113
|
+
lib.graal_create_isolate.restype = ctypes.c_int
|
|
114
|
+
params = None
|
|
115
|
+
iso_args = os.environ.get("ARCADEDB_NATIVE_ISOLATE_ARGS", "").split()
|
|
116
|
+
if iso_args:
|
|
117
|
+
# run-time image options for the isolate (e.g. "-XX:InitialCollectionPolicy=Adaptive -XX:MaximumYoungGenerationSizePercent=20")
|
|
118
|
+
class _Params(ctypes.Structure):
|
|
119
|
+
_fields_ = [
|
|
120
|
+
("version", ctypes.c_int),
|
|
121
|
+
("reserved_address_space_size", ctypes.c_size_t),
|
|
122
|
+
("auxiliary_image_path", ctypes.c_char_p),
|
|
123
|
+
("auxiliary_image_reserved_space_size", ctypes.c_size_t),
|
|
124
|
+
("argc", ctypes.c_int),
|
|
125
|
+
("argv", ctypes.POINTER(ctypes.c_char_p)),
|
|
126
|
+
("pkey", ctypes.c_int),
|
|
127
|
+
("ignore_unrecognized_args", ctypes.c_char),
|
|
128
|
+
("_reserved_4", ctypes.c_char),
|
|
129
|
+
("_reserved_5", ctypes.c_char),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
argv = (ctypes.c_char_p * (len(iso_args) + 1))(
|
|
133
|
+
b"arcadedb", *[a.encode() for a in iso_args]
|
|
134
|
+
)
|
|
135
|
+
params = _Params(version=5, argc=len(argv), argv=argv)
|
|
136
|
+
lib.graal_create_isolate.argtypes[0] = ctypes.POINTER(_Params)
|
|
137
|
+
rc = lib.graal_create_isolate(
|
|
138
|
+
ctypes.byref(params) if params is not None else None,
|
|
139
|
+
ctypes.byref(isolate),
|
|
140
|
+
ctypes.byref(thread),
|
|
141
|
+
)
|
|
142
|
+
if rc != 0:
|
|
143
|
+
raise ArcadeDBError(f"graal_create_isolate failed (rc={rc})")
|
|
144
|
+
lib.graal_attach_thread.argtypes = [
|
|
145
|
+
ctypes.c_void_p,
|
|
146
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
147
|
+
]
|
|
148
|
+
lib.graal_attach_thread.restype = ctypes.c_int
|
|
149
|
+
lib.graal_detach_thread.argtypes = [ctypes.c_void_p]
|
|
150
|
+
lib.graal_detach_thread.restype = ctypes.c_int
|
|
151
|
+
# a Java long is 64 bits on every platform; C long is 32 bits on Windows, so L is long long (the same size as c_long elsewhere)
|
|
152
|
+
P, L, I, S = ctypes.c_void_p, ctypes.c_longlong, ctypes.c_int, ctypes.c_char_p
|
|
153
|
+
sigs = {
|
|
154
|
+
"arcadedb_n_open": [P, S, I],
|
|
155
|
+
"arcadedb_n_close": [P, L, I],
|
|
156
|
+
"arcadedb_n_info": [P, L],
|
|
157
|
+
"arcadedb_n_tx": [P, L, I],
|
|
158
|
+
"arcadedb_n_exec": [P, L, I, S, S, S, I],
|
|
159
|
+
"arcadedb_n_exec_bin": [P, L, I, S, S, S, I, S, L],
|
|
160
|
+
"arcadedb_n_insert_cols": [P, L, S, S, S, L],
|
|
161
|
+
"arcadedb_n_next": [P, L, I],
|
|
162
|
+
"arcadedb_n_rs_close": [P, L],
|
|
163
|
+
"arcadedb_n_cmd": [P, L, I, S, S, S, S, L, I],
|
|
164
|
+
"arcadedb_n_last_error": [P],
|
|
165
|
+
"arcadedb_n_rs_close_many": [P, P, I],
|
|
166
|
+
"arcadedb_n_exec_many": [P, L, S, S, S],
|
|
167
|
+
"arcadedb_n_insert_rows": [P, L, S, S],
|
|
168
|
+
"arcadedb_n_server_start": [P, S],
|
|
169
|
+
"arcadedb_n_server_stop": [P, L],
|
|
170
|
+
"arcadedb_n_server_db": [P, L, S, I],
|
|
171
|
+
"arcadedb_n_diag": [P],
|
|
172
|
+
"arcadedb_n_sample_dump": [P],
|
|
173
|
+
"arcadedb_n_dbset": [P, L, S, S],
|
|
174
|
+
"arcadedb_n_gb_create": [P, L, S],
|
|
175
|
+
"arcadedb_n_gb": [P, L, S, S],
|
|
176
|
+
"arcadedb_n_async": [P, L, S, S],
|
|
177
|
+
"arcadedb_n_import": [P, L, S],
|
|
178
|
+
"arcadedb_n_export": [P, L, S, S, I, S],
|
|
179
|
+
"arcadedb_n_udf_register": [P, S, L, I, I],
|
|
180
|
+
"arcadedb_n_set_upcall": [P, P],
|
|
181
|
+
"arcadedb_free": [P, P],
|
|
182
|
+
}
|
|
183
|
+
for name, argtypes in sigs.items():
|
|
184
|
+
fn = getattr(lib, name, None)
|
|
185
|
+
if fn is None: # a library built before this entry point existed
|
|
186
|
+
continue
|
|
187
|
+
fn.argtypes = argtypes
|
|
188
|
+
fn.restype = (
|
|
189
|
+
None
|
|
190
|
+
if name
|
|
191
|
+
in (
|
|
192
|
+
"arcadedb_free",
|
|
193
|
+
"arcadedb_n_rs_close_many",
|
|
194
|
+
"arcadedb_n_set_upcall",
|
|
195
|
+
)
|
|
196
|
+
else ctypes.c_longlong if name == "arcadedb_n_cmd" else ctypes.c_void_p
|
|
197
|
+
)
|
|
198
|
+
global BINARY_PARAMS, CMD_PATH, CLOSE_BATCH, INSERT_ROWS, EXEC_MANY
|
|
199
|
+
if not hasattr(
|
|
200
|
+
lib, "arcadedb_n_exec_bin"
|
|
201
|
+
): # a library built before the binary path
|
|
202
|
+
BINARY_PARAMS = False
|
|
203
|
+
if not hasattr(lib, "arcadedb_n_cmd"): # a library built before phase 8
|
|
204
|
+
CMD_PATH = False
|
|
205
|
+
if not hasattr(lib, "arcadedb_n_rs_close_many"):
|
|
206
|
+
CLOSE_BATCH = 0
|
|
207
|
+
if not hasattr(lib, "arcadedb_n_insert_rows"):
|
|
208
|
+
INSERT_ROWS = False
|
|
209
|
+
if not hasattr(lib, "arcadedb_n_exec_many"):
|
|
210
|
+
EXEC_MANY = False
|
|
211
|
+
_isolate = isolate
|
|
212
|
+
_creator = threading.get_ident()
|
|
213
|
+
_local.thread = _Thread(thread, owned=False)
|
|
214
|
+
_lib = lib
|
|
215
|
+
return lib
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def loaded() -> bool:
|
|
219
|
+
return _lib is not None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _thread_handle():
|
|
223
|
+
t = getattr(_local, "thread", None)
|
|
224
|
+
if t is None:
|
|
225
|
+
thread = ctypes.c_void_p()
|
|
226
|
+
rc = _lib.graal_attach_thread(_isolate, ctypes.byref(thread))
|
|
227
|
+
if rc != 0:
|
|
228
|
+
raise ArcadeDBError(f"graal_attach_thread failed (rc={rc})")
|
|
229
|
+
t = _local.thread = _Thread(thread, owned=True)
|
|
230
|
+
return t.handle
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def call(name: str, *args) -> dict:
|
|
234
|
+
"""Run one entry point; returns the decoded reply, raises ArcadeDBError-free (callers map errors)."""
|
|
235
|
+
lib = _lib or _load()
|
|
236
|
+
th = _thread_handle()
|
|
237
|
+
ptr = getattr(lib, name)(th, *args)
|
|
238
|
+
try:
|
|
239
|
+
text = ctypes.string_at(ptr).decode("utf-8")
|
|
240
|
+
finally:
|
|
241
|
+
lib.arcadedb_free(th, ptr)
|
|
242
|
+
return text
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def call_bin(name: str, *args):
|
|
246
|
+
"""Like :func:`call` for a reply that may carry a float32 tail (``arcadedb_n_exec*`` with flag 4): returns ``(text, floats)``,
|
|
247
|
+
floats a float32 numpy array or None. The JSON ends with ``,"bin":N}`` and the N floats follow its NUL, 4-byte aligned.
|
|
248
|
+
"""
|
|
249
|
+
lib = _lib or _load()
|
|
250
|
+
th = _thread_handle()
|
|
251
|
+
ptr = getattr(lib, name)(th, *args)
|
|
252
|
+
try:
|
|
253
|
+
raw = ctypes.string_at(ptr)
|
|
254
|
+
floats = None
|
|
255
|
+
if raw.endswith(b"}"):
|
|
256
|
+
i = raw.rfind(b',"bin":', max(0, len(raw) - 40))
|
|
257
|
+
if i >= 0:
|
|
258
|
+
n = int(raw[i + 7 : -1])
|
|
259
|
+
import numpy as np
|
|
260
|
+
|
|
261
|
+
off = (len(raw) + 1 + 3) & ~3
|
|
262
|
+
floats = np.frombuffer(
|
|
263
|
+
ctypes.string_at(ptr + off, 4 * n), dtype=np.float32
|
|
264
|
+
)
|
|
265
|
+
raw = raw[:i] + b"}"
|
|
266
|
+
finally:
|
|
267
|
+
lib.arcadedb_free(th, ptr)
|
|
268
|
+
return raw.decode("utf-8"), floats
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def call_void(name: str, *args) -> None:
|
|
272
|
+
"""Run an entry point that returns nothing (``arcadedb_n_set_upcall``)."""
|
|
273
|
+
lib = _lib or _load()
|
|
274
|
+
getattr(lib, name)(_thread_handle(), *args)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def b(value) -> bytes:
|
|
278
|
+
return value.encode("utf-8") if isinstance(value, str) else value
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
_bytes_cache = {}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def bc(value: str) -> bytes:
|
|
285
|
+
"""UTF-8 of a statement text or language name, remembered (the same few texts cross a million times in a load)."""
|
|
286
|
+
r = _bytes_cache.get(value)
|
|
287
|
+
if r is None:
|
|
288
|
+
r = value.encode("utf-8")
|
|
289
|
+
if len(_bytes_cache) < 4096:
|
|
290
|
+
_bytes_cache[value] = r
|
|
291
|
+
return r
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def cmd(handle, flags, language, command, params, buf, count, first) -> int:
|
|
295
|
+
"""``arcadedb_n_cmd``: the cursor id (> 0), 0 for no cursor, -1 for an error (read it with :func:`last_error`)."""
|
|
296
|
+
lib = _lib or _load()
|
|
297
|
+
t = _local.__dict__.get("thread")
|
|
298
|
+
th = t.handle if t is not None else _thread_handle()
|
|
299
|
+
return lib.arcadedb_n_cmd(
|
|
300
|
+
th, handle, flags, language, command, params, buf, count, first
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def last_error() -> str:
|
|
305
|
+
return call("arcadedb_n_last_error")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# --- result sets closed in batches (one crossing for CLOSE_BATCH cursors)
|
|
309
|
+
_pending = []
|
|
310
|
+
_pending_lock = threading.Lock()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def close_rs(rs_id: int) -> None:
|
|
314
|
+
if CLOSE_BATCH <= 0:
|
|
315
|
+
call("arcadedb_n_rs_close", rs_id)
|
|
316
|
+
return
|
|
317
|
+
_pending.append(rs_id)
|
|
318
|
+
if len(_pending) >= CLOSE_BATCH:
|
|
319
|
+
flush_closes()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def flush_closes() -> None:
|
|
323
|
+
"""Close every cursor whose close is waiting; called before a commit, a rollback and a database close, and when the batch is full."""
|
|
324
|
+
if not _pending or _lib is None:
|
|
325
|
+
return
|
|
326
|
+
with _pending_lock:
|
|
327
|
+
ids = _pending[:]
|
|
328
|
+
del _pending[: len(ids)]
|
|
329
|
+
if ids:
|
|
330
|
+
arr = (ctypes.c_long * len(ids))(
|
|
331
|
+
*ids
|
|
332
|
+
) # arcadedb_n_rs_close_many takes a C `long *` (CLongPointer): c_long on every platform
|
|
333
|
+
_lib.arcadedb_n_rs_close_many(_thread_handle(), arr, len(ids))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def diag() -> dict:
|
|
337
|
+
"""Runtime facts of the image: Java version, jdk.incubator.vector present, CPUs, max heap, jvector provider classes."""
|
|
338
|
+
return json.loads(call("arcadedb_n_diag"))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Record wrappers for the native backend: ``Document``, ``Vertex`` and ``Edge`` as Python objects over SQL statements.
|
|
2
|
+
|
|
3
|
+
The JPype wrappers hold a Java record; here a record is its type, its RID (``None`` until saved) and its properties as a
|
|
4
|
+
dict, and every write is one DSL statement (``CREATE VERTEX .. CONTENT``, ``INSERT .. CONTENT``, ``UPDATE .. MERGE``,
|
|
5
|
+
``DELETE``, ``CREATE EDGE``). They subclass the JPype wrapper classes so ``isinstance`` checks keep their meaning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from ..exceptions import ArcadeDBError
|
|
11
|
+
from ..graph import Document as _BaseDocument
|
|
12
|
+
from ..graph import Edge as _BaseEdge
|
|
13
|
+
from ..graph import Vertex as _BaseVertex
|
|
14
|
+
from . import not_yet
|
|
15
|
+
from .codec import RID
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _java_object():
|
|
19
|
+
from .database import _JavaObject
|
|
20
|
+
|
|
21
|
+
return _JavaObject()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_UNSAVED = "#-1:-1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _q(name: str) -> str:
|
|
28
|
+
return "`" + name.replace("`", "``") + "`"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Mixin:
|
|
32
|
+
"""What the three native wrappers share (state in ``_db``, ``_type``, ``_rid``, ``_props``, ``_mutable``)."""
|
|
33
|
+
|
|
34
|
+
_CREATE = "INSERT INTO {t} CONTENT :row"
|
|
35
|
+
_DELETE = "DELETE FROM {rid}"
|
|
36
|
+
|
|
37
|
+
def __init__(self, database, type_name, rid=None, props=None, mutable=False):
|
|
38
|
+
self._java_document = _java_object()
|
|
39
|
+
self._database = database
|
|
40
|
+
self._db = database
|
|
41
|
+
self._type = type_name
|
|
42
|
+
self._rid = rid or None
|
|
43
|
+
self._props: Dict[str, Any] = dict(props or {})
|
|
44
|
+
self._mutable = mutable or not rid
|
|
45
|
+
self._property_names_cache = None
|
|
46
|
+
|
|
47
|
+
# -- reading
|
|
48
|
+
|
|
49
|
+
def _check_open(self) -> None:
|
|
50
|
+
database = self._database
|
|
51
|
+
if database is not None and database._closed and self._rid:
|
|
52
|
+
raise ArcadeDBError(
|
|
53
|
+
"Database is closed: a record cannot be read after its database was closed. "
|
|
54
|
+
"Read what you need before closing it."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def get(self, name: str, convert_types: bool = True) -> Any:
|
|
58
|
+
self._check_open()
|
|
59
|
+
return self._props.get(name)
|
|
60
|
+
|
|
61
|
+
def get_raw(self, name: str) -> Any:
|
|
62
|
+
return self.get(name)
|
|
63
|
+
|
|
64
|
+
def has_property(self, name: str) -> bool:
|
|
65
|
+
self._check_open()
|
|
66
|
+
return name in self._props
|
|
67
|
+
|
|
68
|
+
def get_property_names(self) -> List[str]:
|
|
69
|
+
self._check_open()
|
|
70
|
+
return list(self._props)
|
|
71
|
+
|
|
72
|
+
def to_dict(self, convert_types: bool = True) -> Dict[str, Any]:
|
|
73
|
+
self._check_open()
|
|
74
|
+
return dict(self._props)
|
|
75
|
+
|
|
76
|
+
def get_rid(self) -> str:
|
|
77
|
+
return self._rid or _UNSAVED
|
|
78
|
+
|
|
79
|
+
def get_identity(self):
|
|
80
|
+
return RID(self.get_rid())
|
|
81
|
+
|
|
82
|
+
def get_type_name(self) -> str:
|
|
83
|
+
self._check_open()
|
|
84
|
+
return self._type
|
|
85
|
+
|
|
86
|
+
def get_java_document(self):
|
|
87
|
+
not_yet("get_java_document")
|
|
88
|
+
|
|
89
|
+
# -- writing
|
|
90
|
+
|
|
91
|
+
def set(self, name: str, value: Any):
|
|
92
|
+
if not self._mutable:
|
|
93
|
+
raise AttributeError(
|
|
94
|
+
f"{type(self).__name__} is immutable. Call .modify() first to get a mutable version."
|
|
95
|
+
)
|
|
96
|
+
self._props[name] = value
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def save(self):
|
|
100
|
+
db = self._db
|
|
101
|
+
if self._rid is None:
|
|
102
|
+
row = db.command(
|
|
103
|
+
"sql", self._CREATE.format(t=_q(self._type)), {"row": self._props}
|
|
104
|
+
).first()
|
|
105
|
+
self._adopt(row)
|
|
106
|
+
else:
|
|
107
|
+
db.command(
|
|
108
|
+
"sql", f"UPDATE {self._rid} MERGE :row", {"row": self._props}
|
|
109
|
+
).close()
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def _adopt(self, row) -> None:
|
|
113
|
+
if row is None or not row.get_rid():
|
|
114
|
+
raise ArcadeDBError(
|
|
115
|
+
f"Failed to save a {self._type} record: the statement returned no record"
|
|
116
|
+
)
|
|
117
|
+
self._rid = row.get_rid()
|
|
118
|
+
self._props = row.to_dict()
|
|
119
|
+
|
|
120
|
+
def delete(self):
|
|
121
|
+
if self._rid is None:
|
|
122
|
+
raise ArcadeDBError("Cannot delete a record that was never saved")
|
|
123
|
+
self._db.command("sql", self._DELETE.format(rid=self._rid)).close()
|
|
124
|
+
|
|
125
|
+
def modify(self):
|
|
126
|
+
self._check_open()
|
|
127
|
+
return type(self)(self._db, self._type, self._rid, self._props, mutable=True)
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return f"<{type(self).__name__} rid={self.get_rid()} type={self._type}>"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Document(_Mixin, _BaseDocument):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Vertex(_Mixin, _BaseVertex):
|
|
138
|
+
_CREATE = "CREATE VERTEX {t} CONTENT :row"
|
|
139
|
+
_DELETE = "DELETE FROM {rid}"
|
|
140
|
+
|
|
141
|
+
def new_edge(self, label: str, target: "Vertex", **kwargs) -> "Edge":
|
|
142
|
+
self._check_open()
|
|
143
|
+
if self._rid is None or getattr(target, "_rid", None) is None:
|
|
144
|
+
raise ArcadeDBError(
|
|
145
|
+
"Both vertices of an edge must be saved before new_edge()"
|
|
146
|
+
)
|
|
147
|
+
sql = f"CREATE EDGE {_q(label)} FROM {self._rid} TO {target._rid}"
|
|
148
|
+
if kwargs:
|
|
149
|
+
sql += " CONTENT :row"
|
|
150
|
+
row = self._db.command(
|
|
151
|
+
"sql", sql, *(({"row": kwargs},) if kwargs else ())
|
|
152
|
+
).first()
|
|
153
|
+
edge = Edge(self._db, label, mutable=False)
|
|
154
|
+
edge._adopt(row)
|
|
155
|
+
return edge
|
|
156
|
+
|
|
157
|
+
def _edges(self, fn: str, labels) -> List["Edge"]:
|
|
158
|
+
self._check_open()
|
|
159
|
+
args = ", ".join("'" + label.replace("'", "\\'") + "'" for label in labels)
|
|
160
|
+
rows = self._db.query("sql", f"SELECT expand({fn}({args})) FROM {self._rid}")
|
|
161
|
+
return [_wrap(self._db, row) for row in rows]
|
|
162
|
+
|
|
163
|
+
def get_out_edges(self, *labels: str) -> List["Edge"]:
|
|
164
|
+
return self._edges("outE", labels)
|
|
165
|
+
|
|
166
|
+
def get_in_edges(self, *labels: str) -> List["Edge"]:
|
|
167
|
+
return self._edges("inE", labels)
|
|
168
|
+
|
|
169
|
+
def get_both_edges(self, *labels: str) -> List["Edge"]:
|
|
170
|
+
return self._edges("bothE", labels)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Edge(_Mixin, _BaseEdge):
|
|
174
|
+
|
|
175
|
+
def _end(self, fn: str) -> "Vertex":
|
|
176
|
+
self._check_open()
|
|
177
|
+
row = self._db.query("sql", f"SELECT expand({fn}()) FROM {self._rid}").first()
|
|
178
|
+
return _wrap(self._db, row)
|
|
179
|
+
|
|
180
|
+
def get_in(self) -> Vertex:
|
|
181
|
+
return self._end("inV")
|
|
182
|
+
|
|
183
|
+
def get_out(self) -> Vertex:
|
|
184
|
+
return self._end("outV")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _wrap(database, row):
|
|
188
|
+
"""The wrapper for a result row that is a record (vertex, edge or document); None for a projection."""
|
|
189
|
+
meta = row._meta
|
|
190
|
+
if not meta:
|
|
191
|
+
return None
|
|
192
|
+
rid, type_name, kind = meta
|
|
193
|
+
cls = {"v": Vertex, "e": Edge}.get(kind, Document)
|
|
194
|
+
return cls(database, type_name, rid, row._props)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _wrap_dict(database, record: dict, kind: str = "d"):
|
|
198
|
+
"""The wrapper for a nested record the engine sent as a dict with ``@rid``, ``@type`` and ``@cat`` members."""
|
|
199
|
+
props = dict(record)
|
|
200
|
+
rid = props.pop("@rid", None)
|
|
201
|
+
type_name = props.pop("@type", None)
|
|
202
|
+
kind = props.pop("@cat", kind)
|
|
203
|
+
cls = {"v": Vertex, "e": Edge}.get(kind, Document)
|
|
204
|
+
return cls(database, type_name, rid, props)
|