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,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,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: arcadedb_native/_native/libarcadedb.dll
3
+ size: 101216256 bytes
4
+ sha256: 3f396acb50062e6d3ed59de0f0b05b76c68728d6e0a2634b087ef36143e7c169
@@ -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)