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,1356 @@
|
|
|
1
|
+
"""NativeDatabase, NativeDatabaseFactory, NativeResultSet and NativeResult: the DSL-first core over libarcadedb."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time as _time
|
|
7
|
+
from collections import deque
|
|
8
|
+
from datetime import date, datetime
|
|
9
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..exceptions import ArcadeDBError
|
|
12
|
+
from ..transactions import TransactionContext
|
|
13
|
+
from ..type_conversion import json_bulk_dumps
|
|
14
|
+
from . import lib, not_yet
|
|
15
|
+
from .codec import _default as codec_default
|
|
16
|
+
from .codec import _prep as _prep_row
|
|
17
|
+
from .codec import encode_params, json_native, loads
|
|
18
|
+
|
|
19
|
+
_LAZY_COMMAND_ROWS = os.environ.get("ARCADEDB_NATIVE_LAZY_CMD", "1") != "0"
|
|
20
|
+
_BINARY_REPLY = os.environ.get("ARCADEDB_NATIVE_BIN_REPLY", "1") != "0"
|
|
21
|
+
_INSERT_MANY_CHUNK = 10_000
|
|
22
|
+
_FIRST_BATCH = 256
|
|
23
|
+
_EPOCH_DAY_ORDINAL = date(1970, 1, 1).toordinal()
|
|
24
|
+
_NEXT_BATCH = 2048
|
|
25
|
+
_KIND = {"v": "v", "e": "e", "d": "d"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _error(prefix: str, reply: dict) -> ArcadeDBError:
|
|
29
|
+
"""The ArcadeDBError for a failed reply, worded like the JPype path (``prefix: java.class: message``)."""
|
|
30
|
+
chain = reply.get("chain") or []
|
|
31
|
+
head = f"{reply.get('cls')}: {reply.get('error')}"
|
|
32
|
+
exc = ArcadeDBError(f"{prefix}: {head}" if prefix else head)
|
|
33
|
+
exc.native = reply
|
|
34
|
+
exc.java_chain = chain
|
|
35
|
+
return exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_error(reply: dict) -> ArcadeDBError:
|
|
39
|
+
exc = ArcadeDBError(
|
|
40
|
+
f"Reading the result set failed: {reply.get('cls')}: {reply.get('error')}"
|
|
41
|
+
)
|
|
42
|
+
exc.native = reply
|
|
43
|
+
exc.java_chain = reply.get("chain") or []
|
|
44
|
+
return exc
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _call(prefix: str, name: str, *args) -> dict:
|
|
48
|
+
reply = json.loads(lib.call(name, *args))
|
|
49
|
+
if not reply.get("ok"):
|
|
50
|
+
raise _error(prefix, reply)
|
|
51
|
+
return reply
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NativeResult:
|
|
55
|
+
"""One row. Values are already Python types (datetime, Decimal, lists, dicts, RID text)."""
|
|
56
|
+
|
|
57
|
+
__slots__ = ("_props", "_meta", "_database")
|
|
58
|
+
|
|
59
|
+
def __init__(self, props: Dict[str, Any], meta, database=None):
|
|
60
|
+
self._props = props
|
|
61
|
+
self._meta = meta or None
|
|
62
|
+
self._database = database
|
|
63
|
+
|
|
64
|
+
def _check_open(self) -> None:
|
|
65
|
+
database = self._database
|
|
66
|
+
if database is not None and database._closed and self._meta is not None:
|
|
67
|
+
raise ArcadeDBError(
|
|
68
|
+
"Database is closed: a record row cannot be read after its database was closed. "
|
|
69
|
+
"Read what you need before closing it."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def has_property(self, name: str) -> bool:
|
|
73
|
+
self._check_open()
|
|
74
|
+
return name in self._props
|
|
75
|
+
|
|
76
|
+
def get(self, name: str, convert_types: bool = True) -> Any:
|
|
77
|
+
self._check_open()
|
|
78
|
+
return self._props.get(name)
|
|
79
|
+
|
|
80
|
+
def get_raw(self, name: str) -> Any:
|
|
81
|
+
return self.get(name)
|
|
82
|
+
|
|
83
|
+
def get_rid(self) -> Optional[str]:
|
|
84
|
+
return self._meta[0] if self._meta else None
|
|
85
|
+
|
|
86
|
+
def _record(self, kinds):
|
|
87
|
+
from .records import _wrap
|
|
88
|
+
|
|
89
|
+
self._check_open()
|
|
90
|
+
if not self._meta or self._meta[2] not in kinds:
|
|
91
|
+
return None
|
|
92
|
+
return _wrap(self._database, self)
|
|
93
|
+
|
|
94
|
+
def get_vertex(self):
|
|
95
|
+
return self._record("v")
|
|
96
|
+
|
|
97
|
+
def get_edge(self):
|
|
98
|
+
return self._record("e")
|
|
99
|
+
|
|
100
|
+
def get_element(self):
|
|
101
|
+
return self._record("vde")
|
|
102
|
+
|
|
103
|
+
def get_property_names(self) -> List[str]:
|
|
104
|
+
self._check_open()
|
|
105
|
+
return list(self._props)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def property_names(self) -> List[str]:
|
|
109
|
+
return self.get_property_names()
|
|
110
|
+
|
|
111
|
+
def to_dict(self, convert_types: bool = True) -> Dict[str, Any]:
|
|
112
|
+
self._check_open()
|
|
113
|
+
return dict(self._props)
|
|
114
|
+
|
|
115
|
+
def to_json(self) -> str:
|
|
116
|
+
self._check_open()
|
|
117
|
+
out = json_native(self._props)
|
|
118
|
+
for key, value in self._props.items():
|
|
119
|
+
if isinstance(value, date) and not isinstance(value, datetime):
|
|
120
|
+
out[key] = (
|
|
121
|
+
value.toordinal() - _EPOCH_DAY_ORDINAL
|
|
122
|
+
) # as the engine writes a DATE property: days since the epoch
|
|
123
|
+
if self._meta:
|
|
124
|
+
rid, type_name, kind = self._meta
|
|
125
|
+
out = dict(out)
|
|
126
|
+
out["@cat"] = kind
|
|
127
|
+
out["@type"] = type_name
|
|
128
|
+
out["@rid"] = rid
|
|
129
|
+
return json.dumps(out, default=str, separators=(",", ":"))
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
names = list(self._props)
|
|
133
|
+
preview = ", ".join(repr(n) for n in names[:3]) + (
|
|
134
|
+
", ..." if len(names) > 3 else ""
|
|
135
|
+
)
|
|
136
|
+
rid = self.get_rid()
|
|
137
|
+
return (
|
|
138
|
+
f"Result(rid={rid!r}, properties=[{preview}])"
|
|
139
|
+
if rid
|
|
140
|
+
else f"Result(properties=[{preview}])"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class NativeResultSet:
|
|
145
|
+
"""Cursor over a statement's rows, fetched in batches through ``arcadedb_n_next``."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, database, reply: dict):
|
|
148
|
+
self._database = database # strong reference: a result keeps its database alive
|
|
149
|
+
self._rs = reply["rs"]
|
|
150
|
+
self._buf = deque(zip(reply["rows"], reply["meta"]))
|
|
151
|
+
self._more = not reply["drained"]
|
|
152
|
+
self._closed = False
|
|
153
|
+
self._exhausted = False
|
|
154
|
+
# the engine computes lazily: an error met while the first rows were read is raised when the rows before it
|
|
155
|
+
# have been consumed, as it is on the JPype path (the cursor is already finished on the Java side)
|
|
156
|
+
self._pending_error = reply.get("rerr")
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _lazy(cls, database, rs_id: int) -> "NativeResultSet":
|
|
160
|
+
"""The result of arcadedb_n_cmd: a cursor id and nothing read yet (id 0: no cursor)."""
|
|
161
|
+
self = cls.__new__(cls)
|
|
162
|
+
self._database = database
|
|
163
|
+
self._rs = rs_id
|
|
164
|
+
self._buf = deque()
|
|
165
|
+
self._more = rs_id > 0
|
|
166
|
+
self._closed = False
|
|
167
|
+
self._exhausted = False
|
|
168
|
+
self._pending_error = None
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
# -- state
|
|
172
|
+
|
|
173
|
+
def _readable(self) -> bool:
|
|
174
|
+
database = self._database
|
|
175
|
+
if database is not None and database._closed and not self._exhausted:
|
|
176
|
+
raise ArcadeDBError(
|
|
177
|
+
"Database is closed: the rows of this result set cannot be read after their database was closed. "
|
|
178
|
+
"Read them before closing the database."
|
|
179
|
+
)
|
|
180
|
+
if not self._closed:
|
|
181
|
+
return True
|
|
182
|
+
if self._exhausted:
|
|
183
|
+
return False
|
|
184
|
+
raise ArcadeDBError(
|
|
185
|
+
"This result set was closed before all its rows were read (by first(), one(), close(), or leaving its "
|
|
186
|
+
"with block), so the rows it had not returned are gone. Run the query again to read them."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _fill(self) -> bool:
|
|
190
|
+
"""Fetch the next batch into the buffer; False once the cursor is drained."""
|
|
191
|
+
if self._pending_error is not None:
|
|
192
|
+
err, self._pending_error = self._pending_error, None
|
|
193
|
+
self._more = False
|
|
194
|
+
self._closed = True
|
|
195
|
+
self._exhausted = True
|
|
196
|
+
raise _read_error(err)
|
|
197
|
+
if not self._more:
|
|
198
|
+
return False
|
|
199
|
+
try:
|
|
200
|
+
reply = loads(lib.call("arcadedb_n_next", self._rs, _NEXT_BATCH))
|
|
201
|
+
except ArcadeDBError:
|
|
202
|
+
raise
|
|
203
|
+
if not reply.get("ok"):
|
|
204
|
+
self._more = False
|
|
205
|
+
self._closed = True
|
|
206
|
+
self._exhausted = True
|
|
207
|
+
self._rs = 0
|
|
208
|
+
raise ArcadeDBError(
|
|
209
|
+
f"Reading the result set failed: {reply.get('cls')}: {reply.get('error')}"
|
|
210
|
+
)
|
|
211
|
+
self._buf.extend(zip(reply["rows"], reply["meta"]))
|
|
212
|
+
self._more = not reply["drained"]
|
|
213
|
+
self._pending_error = reply.get("rerr")
|
|
214
|
+
return bool(self._buf) or self._pending_error is not None
|
|
215
|
+
|
|
216
|
+
def _finish(self) -> None:
|
|
217
|
+
self._exhausted = True
|
|
218
|
+
self.close()
|
|
219
|
+
|
|
220
|
+
def close(self) -> None:
|
|
221
|
+
if self._closed:
|
|
222
|
+
return
|
|
223
|
+
self._closed = True
|
|
224
|
+
if self._more and self._rs:
|
|
225
|
+
try:
|
|
226
|
+
lib.close_rs(self._rs)
|
|
227
|
+
except Exception: # nosec B110 - best effort
|
|
228
|
+
pass
|
|
229
|
+
self._more = False
|
|
230
|
+
self._buf.clear()
|
|
231
|
+
|
|
232
|
+
# -- reading
|
|
233
|
+
|
|
234
|
+
def __iter__(self) -> Iterator[NativeResult]:
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def __next__(self) -> NativeResult:
|
|
238
|
+
if self._readable():
|
|
239
|
+
while True:
|
|
240
|
+
if self._buf:
|
|
241
|
+
props, meta = self._buf.popleft()
|
|
242
|
+
return NativeResult(props, meta, self._database)
|
|
243
|
+
if not self._fill():
|
|
244
|
+
break
|
|
245
|
+
if not self._closed:
|
|
246
|
+
self._finish()
|
|
247
|
+
raise StopIteration
|
|
248
|
+
|
|
249
|
+
def _drain_dicts(self) -> List[Dict[str, Any]]:
|
|
250
|
+
out: List[Dict[str, Any]] = []
|
|
251
|
+
if not self._readable():
|
|
252
|
+
return out
|
|
253
|
+
while True:
|
|
254
|
+
out.extend(p for p, _ in self._buf)
|
|
255
|
+
self._buf.clear()
|
|
256
|
+
if not self._fill():
|
|
257
|
+
break
|
|
258
|
+
self._finish()
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
def to_list(self, convert_types: bool = True) -> List[Dict[str, Any]]:
|
|
262
|
+
return self._drain_dicts()
|
|
263
|
+
|
|
264
|
+
def iter_dicts(self, convert_types: bool = True) -> Iterator[Dict[str, Any]]:
|
|
265
|
+
for row in self:
|
|
266
|
+
yield row.to_dict()
|
|
267
|
+
|
|
268
|
+
def to_json_list(self, batch_size: int = 10_000) -> List[Dict[str, Any]]:
|
|
269
|
+
if int(batch_size) < 1:
|
|
270
|
+
raise ValueError("batch_size must be at least 1")
|
|
271
|
+
return [json_native(row) for row in self._drain_dicts()]
|
|
272
|
+
|
|
273
|
+
def iter_json_batches(self, batch_size: int = 10_000, **kwargs):
|
|
274
|
+
if int(batch_size) < 1:
|
|
275
|
+
raise ValueError("batch_size must be at least 1")
|
|
276
|
+
rows = self.to_json_list()
|
|
277
|
+
for i in range(0, len(rows), batch_size):
|
|
278
|
+
yield rows[i : i + batch_size]
|
|
279
|
+
|
|
280
|
+
def to_dataframe(self, convert_types: bool = True):
|
|
281
|
+
import pandas as pd
|
|
282
|
+
|
|
283
|
+
return pd.DataFrame(self._drain_dicts())
|
|
284
|
+
|
|
285
|
+
def to_columns(self, batch_size: int = 25_000, columns=None):
|
|
286
|
+
"""Columns as the JPype path returns them (numpy int64/float64/bool/datetime64[ms] arrays, lists for text and
|
|
287
|
+
JSON-typed values, an object array of Decimal), built in Python from the rows: a columnar JSON path, no
|
|
288
|
+
binary buffer. None when numpy is missing."""
|
|
289
|
+
try:
|
|
290
|
+
import numpy # noqa: F401
|
|
291
|
+
except ImportError:
|
|
292
|
+
return None
|
|
293
|
+
from .columns import to_columns
|
|
294
|
+
|
|
295
|
+
return to_columns(self._drain_dicts(), columns)
|
|
296
|
+
|
|
297
|
+
def to_arrow(self, batch_size: int = 25_000, columns=None):
|
|
298
|
+
"""A ``pyarrow.Table`` with the types of :meth:`to_columns` but nulls kept (None when pyarrow is missing)."""
|
|
299
|
+
try:
|
|
300
|
+
import numpy # noqa: F401
|
|
301
|
+
import pyarrow # noqa: F401
|
|
302
|
+
except ImportError:
|
|
303
|
+
return None
|
|
304
|
+
from .columns import to_arrow
|
|
305
|
+
|
|
306
|
+
return to_arrow(self._drain_dicts(), columns)
|
|
307
|
+
|
|
308
|
+
def to_polars(self, batch_size: int = 25_000, columns=None):
|
|
309
|
+
try:
|
|
310
|
+
import polars as pl
|
|
311
|
+
except ImportError as exc:
|
|
312
|
+
raise ImportError(
|
|
313
|
+
"polars is required for to_polars(). "
|
|
314
|
+
"Install with: pip install 'arcadedb-embedded-native[polars]'"
|
|
315
|
+
) from exc
|
|
316
|
+
table = self.to_arrow(batch_size, columns)
|
|
317
|
+
if table is None:
|
|
318
|
+
raise ImportError(
|
|
319
|
+
"to_polars() reads through Arrow, which needs pyarrow and numpy. "
|
|
320
|
+
"Install with: pip install 'arcadedb-embedded-native[polars]'"
|
|
321
|
+
)
|
|
322
|
+
return pl.from_arrow(table)
|
|
323
|
+
|
|
324
|
+
def iter_chunks(self, size: int = 1000, convert_types: bool = True):
|
|
325
|
+
chunk = []
|
|
326
|
+
for row in self.iter_dicts(convert_types=convert_types):
|
|
327
|
+
chunk.append(row)
|
|
328
|
+
if len(chunk) >= size:
|
|
329
|
+
yield chunk
|
|
330
|
+
chunk = []
|
|
331
|
+
if chunk:
|
|
332
|
+
yield chunk
|
|
333
|
+
|
|
334
|
+
def count(self) -> int:
|
|
335
|
+
n = 0
|
|
336
|
+
for _ in self:
|
|
337
|
+
n += 1
|
|
338
|
+
return n
|
|
339
|
+
|
|
340
|
+
def first(self) -> Optional[NativeResult]:
|
|
341
|
+
try:
|
|
342
|
+
return next(iter(self))
|
|
343
|
+
except StopIteration:
|
|
344
|
+
return None
|
|
345
|
+
finally:
|
|
346
|
+
self.close()
|
|
347
|
+
|
|
348
|
+
def one(self) -> NativeResult:
|
|
349
|
+
iterator = iter(self)
|
|
350
|
+
try:
|
|
351
|
+
try:
|
|
352
|
+
result = next(iterator)
|
|
353
|
+
except StopIteration as exc:
|
|
354
|
+
raise ValueError("Query returned no results") from exc
|
|
355
|
+
try:
|
|
356
|
+
next(iterator)
|
|
357
|
+
raise ValueError("Query returned multiple results")
|
|
358
|
+
except StopIteration:
|
|
359
|
+
return result
|
|
360
|
+
finally:
|
|
361
|
+
self.close()
|
|
362
|
+
|
|
363
|
+
def __enter__(self):
|
|
364
|
+
return self
|
|
365
|
+
|
|
366
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
367
|
+
self.close()
|
|
368
|
+
|
|
369
|
+
def __del__(self):
|
|
370
|
+
try:
|
|
371
|
+
self.close()
|
|
372
|
+
except Exception: # nosec B110
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class NativeDatabase:
|
|
377
|
+
"""The DSL-first database handle: statements, transactions, lifecycle. See the package docstring for the rest."""
|
|
378
|
+
|
|
379
|
+
def __init__(self, handle: int, owned_by_server: bool = False):
|
|
380
|
+
self._h = handle
|
|
381
|
+
self._closed = False
|
|
382
|
+
self._server_owned = owned_by_server
|
|
383
|
+
self._async_executors: list = []
|
|
384
|
+
self._vector_meta: dict = {}
|
|
385
|
+
self._vector_dimensions: dict = {}
|
|
386
|
+
self._vector_id_properties: dict = {}
|
|
387
|
+
|
|
388
|
+
# -- lifecycle
|
|
389
|
+
|
|
390
|
+
def __enter__(self):
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
394
|
+
self.close()
|
|
395
|
+
|
|
396
|
+
def _check_not_closed(self):
|
|
397
|
+
if self._closed:
|
|
398
|
+
raise ArcadeDBError("Database is closed")
|
|
399
|
+
|
|
400
|
+
def close(self):
|
|
401
|
+
if self._closed:
|
|
402
|
+
return
|
|
403
|
+
self._close_async_executors()
|
|
404
|
+
if self._server_owned:
|
|
405
|
+
# owned by the server: the engine refuses the close, and this handle is done either way
|
|
406
|
+
self._closed = True
|
|
407
|
+
return
|
|
408
|
+
try:
|
|
409
|
+
lib.flush_closes()
|
|
410
|
+
_call("Failed to close database", "arcadedb_n_close", self._h, 0)
|
|
411
|
+
finally:
|
|
412
|
+
self._closed = True
|
|
413
|
+
|
|
414
|
+
def _discard_async_executor(self, executor) -> None:
|
|
415
|
+
try:
|
|
416
|
+
self._async_executors.remove(executor)
|
|
417
|
+
except ValueError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
def _close_async_executors(self):
|
|
421
|
+
first_error = None
|
|
422
|
+
while self._async_executors:
|
|
423
|
+
executor = self._async_executors.pop()
|
|
424
|
+
try:
|
|
425
|
+
executor.close()
|
|
426
|
+
except Exception as e:
|
|
427
|
+
first_error = first_error or ArcadeDBError(
|
|
428
|
+
f"Failed to close async executor: {e}"
|
|
429
|
+
)
|
|
430
|
+
if first_error is not None:
|
|
431
|
+
raise first_error
|
|
432
|
+
|
|
433
|
+
def drop(self):
|
|
434
|
+
self._check_not_closed()
|
|
435
|
+
lib.flush_closes()
|
|
436
|
+
_call("Failed to drop database", "arcadedb_n_close", self._h, 1)
|
|
437
|
+
self._closed = True
|
|
438
|
+
|
|
439
|
+
def __del__(self):
|
|
440
|
+
try:
|
|
441
|
+
self.close()
|
|
442
|
+
except Exception: # nosec B110
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
def _info(self) -> dict:
|
|
446
|
+
return _call("Failed to read the database", "arcadedb_n_info", self._h)
|
|
447
|
+
|
|
448
|
+
def is_open(self) -> bool:
|
|
449
|
+
return not self._closed and self._info()["open"]
|
|
450
|
+
|
|
451
|
+
def get_name(self) -> str:
|
|
452
|
+
self._check_not_closed()
|
|
453
|
+
return self._info()["name"]
|
|
454
|
+
|
|
455
|
+
def get_database_path(self) -> str:
|
|
456
|
+
self._check_not_closed()
|
|
457
|
+
return self._info()["path"]
|
|
458
|
+
|
|
459
|
+
def is_read_only(self) -> bool:
|
|
460
|
+
self._check_not_closed()
|
|
461
|
+
return self._info()["mode"] == "READ_ONLY"
|
|
462
|
+
|
|
463
|
+
# -- statements
|
|
464
|
+
|
|
465
|
+
def _exec(
|
|
466
|
+
self, flags: int, prefix: str, language: str, command: str, args
|
|
467
|
+
) -> NativeResultSet:
|
|
468
|
+
self._check_not_closed()
|
|
469
|
+
if flags & 1 and _LAZY_COMMAND_ROWS and lib.CMD_PATH:
|
|
470
|
+
# phase 8: the cursor id comes back as a long (no reply text to free and decode); the rows are read late or never
|
|
471
|
+
text, buf, count = encode_params(args, binary=True)
|
|
472
|
+
rid = lib.cmd(
|
|
473
|
+
self._h,
|
|
474
|
+
1,
|
|
475
|
+
lib.bc(language),
|
|
476
|
+
lib.bc(command),
|
|
477
|
+
lib.bc(text),
|
|
478
|
+
buf,
|
|
479
|
+
count,
|
|
480
|
+
_FIRST_BATCH,
|
|
481
|
+
)
|
|
482
|
+
if rid < 0:
|
|
483
|
+
raise _error(prefix, json.loads(lib.last_error()))
|
|
484
|
+
return NativeResultSet._lazy(self, rid)
|
|
485
|
+
# phase 7 (arcadedb_n_exec flags): 2 = a command's rows are pulled but encoded only when read, 4 = float arrays come back as a float32 tail
|
|
486
|
+
if flags & 1 and _LAZY_COMMAND_ROWS:
|
|
487
|
+
flags |= 2
|
|
488
|
+
if _BINARY_REPLY:
|
|
489
|
+
flags |= 4
|
|
490
|
+
if lib.BINARY_PARAMS:
|
|
491
|
+
text, buf, count = encode_params(args, binary=True)
|
|
492
|
+
if buf is not None:
|
|
493
|
+
raw, floats = lib.call_bin(
|
|
494
|
+
"arcadedb_n_exec_bin",
|
|
495
|
+
self._h,
|
|
496
|
+
flags,
|
|
497
|
+
lib.b(language),
|
|
498
|
+
lib.b(command),
|
|
499
|
+
lib.b(text),
|
|
500
|
+
_FIRST_BATCH,
|
|
501
|
+
buf,
|
|
502
|
+
count,
|
|
503
|
+
)
|
|
504
|
+
else:
|
|
505
|
+
raw, floats = lib.call_bin(
|
|
506
|
+
"arcadedb_n_exec",
|
|
507
|
+
self._h,
|
|
508
|
+
flags,
|
|
509
|
+
lib.b(language),
|
|
510
|
+
lib.b(command),
|
|
511
|
+
lib.b(text),
|
|
512
|
+
_FIRST_BATCH,
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
raw, floats = lib.call_bin(
|
|
516
|
+
"arcadedb_n_exec",
|
|
517
|
+
self._h,
|
|
518
|
+
flags,
|
|
519
|
+
lib.b(language),
|
|
520
|
+
lib.b(command),
|
|
521
|
+
lib.b(encode_params(args)),
|
|
522
|
+
_FIRST_BATCH,
|
|
523
|
+
)
|
|
524
|
+
reply = loads(raw, floats)
|
|
525
|
+
if not reply.get("ok"):
|
|
526
|
+
raise _error(prefix, reply)
|
|
527
|
+
return NativeResultSet(self, reply)
|
|
528
|
+
|
|
529
|
+
def query(self, language: str, command: str, *args) -> NativeResultSet:
|
|
530
|
+
"""Run a read statement. Parameters bind as on the JPype path (``?`` positional, ``:name`` from one dict)."""
|
|
531
|
+
return self._exec(0, "Query failed", language, command, args)
|
|
532
|
+
|
|
533
|
+
def command(self, language: str, command: str, *args) -> NativeResultSet:
|
|
534
|
+
"""Run a statement that may write."""
|
|
535
|
+
return self._exec(1, "Command failed", language, command, args)
|
|
536
|
+
|
|
537
|
+
# -- transactions
|
|
538
|
+
|
|
539
|
+
def _tx(self, op: int, prefix: str) -> dict:
|
|
540
|
+
self._check_not_closed()
|
|
541
|
+
if (op == 1 or op == 2) and lib.CLOSE_BATCH:
|
|
542
|
+
lib.flush_closes()
|
|
543
|
+
return _call(prefix, "arcadedb_n_tx", self._h, op)
|
|
544
|
+
|
|
545
|
+
def begin(self):
|
|
546
|
+
self._tx(0, "Failed to begin transaction")
|
|
547
|
+
|
|
548
|
+
def commit(self):
|
|
549
|
+
self._tx(1, "Failed to commit transaction")
|
|
550
|
+
|
|
551
|
+
def rollback(self):
|
|
552
|
+
self._tx(2, "Failed to rollback transaction")
|
|
553
|
+
|
|
554
|
+
def transaction(self) -> TransactionContext:
|
|
555
|
+
return TransactionContext(self)
|
|
556
|
+
|
|
557
|
+
def is_transaction_active(self) -> bool:
|
|
558
|
+
return self._tx(3, "Failed to check transaction status")["active"]
|
|
559
|
+
|
|
560
|
+
def _transaction_stack_mark(self):
|
|
561
|
+
state = self._tx(3, "Failed to read the transaction state")
|
|
562
|
+
return state["active"], state["depth"]
|
|
563
|
+
|
|
564
|
+
def _transaction_commit_mark(self):
|
|
565
|
+
"""The commit count of the current transaction (the witness that a BATCH boundary published part of its work)."""
|
|
566
|
+
return self._tx(3, "Failed to read the transaction state")["commits"]
|
|
567
|
+
|
|
568
|
+
def _end_transactions_opened_since(self, mark):
|
|
569
|
+
was_active, depth_before = mark
|
|
570
|
+
first_error = None
|
|
571
|
+
state = self._tx(3, "Failed to read the transaction state")
|
|
572
|
+
for _ in range(state["depth"] - depth_before):
|
|
573
|
+
if not self.is_transaction_active():
|
|
574
|
+
break
|
|
575
|
+
try:
|
|
576
|
+
self.rollback()
|
|
577
|
+
except Exception as e:
|
|
578
|
+
first_error = first_error or e
|
|
579
|
+
if not was_active and self.is_transaction_active():
|
|
580
|
+
try:
|
|
581
|
+
self.rollback()
|
|
582
|
+
except Exception as e:
|
|
583
|
+
first_error = first_error or e
|
|
584
|
+
if first_error is not None:
|
|
585
|
+
raise first_error
|
|
586
|
+
|
|
587
|
+
def run_in_transaction(self, fn, retries: int = 12, backoff_s: float = 0.005):
|
|
588
|
+
"""``fn`` inside a transaction, retried on NeedRetry conflicts (see the JPype ``Database.run_in_transaction``)."""
|
|
589
|
+
duplicated_key_retried = False
|
|
590
|
+
for attempt in range(retries + 1):
|
|
591
|
+
mark = self._transaction_stack_mark()
|
|
592
|
+
commits_at_start = None
|
|
593
|
+
try:
|
|
594
|
+
self.begin()
|
|
595
|
+
commits_at_start = self._transaction_commit_mark()
|
|
596
|
+
result = fn()
|
|
597
|
+
self.commit()
|
|
598
|
+
return result
|
|
599
|
+
except BaseException as e:
|
|
600
|
+
try:
|
|
601
|
+
self._end_transactions_opened_since(mark)
|
|
602
|
+
except Exception: # nosec B110
|
|
603
|
+
pass
|
|
604
|
+
if attempt >= retries or not isinstance(e, Exception):
|
|
605
|
+
raise
|
|
606
|
+
reply = getattr(e, "native", None) or {}
|
|
607
|
+
kind = reply.get("retry")
|
|
608
|
+
if kind is None:
|
|
609
|
+
raise
|
|
610
|
+
if kind == "duplicated_key":
|
|
611
|
+
if duplicated_key_retried:
|
|
612
|
+
raise
|
|
613
|
+
duplicated_key_retried = True
|
|
614
|
+
if commits_at_start is not None:
|
|
615
|
+
try:
|
|
616
|
+
if self._transaction_commit_mark() != commits_at_start:
|
|
617
|
+
raise
|
|
618
|
+
except ArcadeDBError:
|
|
619
|
+
pass
|
|
620
|
+
_time.sleep(
|
|
621
|
+
max(backoff_s * (attempt + 1), reply.get("retry_ms", 0) / 1000.0)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# -- small helpers over statements (DSL-first: no Java record objects)
|
|
625
|
+
|
|
626
|
+
def count_type(self, type_name: str) -> int:
|
|
627
|
+
self._check_not_closed()
|
|
628
|
+
try:
|
|
629
|
+
row = self.query("sql", f"SELECT count(*) AS c FROM `{type_name}`").first()
|
|
630
|
+
except ArcadeDBError as e:
|
|
631
|
+
if "was not found" in str(e):
|
|
632
|
+
return 0
|
|
633
|
+
raise ArcadeDBError(f"Failed to count type '{type_name}': {e}") from e
|
|
634
|
+
return int(row.get("c")) if row is not None else 0
|
|
635
|
+
|
|
636
|
+
def lookup_by_rid(self, rid: str):
|
|
637
|
+
"""The record at ``rid`` as a Document, Vertex or Edge wrapper."""
|
|
638
|
+
self._check_not_closed()
|
|
639
|
+
row = self.query("sql", f"SELECT FROM {rid}").first()
|
|
640
|
+
if row is None:
|
|
641
|
+
raise ArcadeDBError(f"Failed to lookup RID '{rid}': record not found")
|
|
642
|
+
return row.get_element()
|
|
643
|
+
|
|
644
|
+
def lookup_by_key(self, type_name: str, keys: List[str], values: List[Any]):
|
|
645
|
+
self._check_not_closed()
|
|
646
|
+
where = " AND ".join(f"`{k}` = ?" for k in keys)
|
|
647
|
+
try:
|
|
648
|
+
row = self.query(
|
|
649
|
+
"sql", f"SELECT FROM `{type_name}` WHERE {where} LIMIT 1", list(values)
|
|
650
|
+
).first()
|
|
651
|
+
return row.get_element() if row is not None else None
|
|
652
|
+
except ArcadeDBError as e:
|
|
653
|
+
raise ArcadeDBError(f"Failed to lookup by key in '{type_name}': {e}") from e
|
|
654
|
+
|
|
655
|
+
def insert_many(
|
|
656
|
+
self, type_name: str, rows, commit_every: int = 10_000, parallel: bool = False
|
|
657
|
+
) -> int:
|
|
658
|
+
"""Bulk insert. Rows go in chunks of 10,000, one JSON text and one crossing per chunk (``arcadedb_n_insert_rows``: a
|
|
659
|
+
newDocument/set/save loop inside the image, as ``DocumentBatcher.insertManyJson`` on the JPype path); a chunk with a value the
|
|
660
|
+
JSON text cannot carry unchanged goes row by row through ``INSERT INTO t CONTENT :row``. Inside the caller's transaction nothing is
|
|
661
|
+
committed; otherwise every ``commit_every`` rows are one transaction (0: all rows in one).
|
|
662
|
+
"""
|
|
663
|
+
self._check_not_closed()
|
|
664
|
+
if parallel:
|
|
665
|
+
return self._insert_many_parallel(type_name, rows)
|
|
666
|
+
sql = f"INSERT INTO `{type_name}` CONTENT :row"
|
|
667
|
+
rows = iter(rows)
|
|
668
|
+
was_active = self.is_transaction_active()
|
|
669
|
+
every = int(commit_every)
|
|
670
|
+
n = 0
|
|
671
|
+
try:
|
|
672
|
+
if not was_active:
|
|
673
|
+
self.begin()
|
|
674
|
+
while True:
|
|
675
|
+
size = _INSERT_MANY_CHUNK
|
|
676
|
+
if not was_active and every > 0:
|
|
677
|
+
size = min(size, every - n % every)
|
|
678
|
+
chunk = list(itertools.islice(rows, size))
|
|
679
|
+
if not chunk:
|
|
680
|
+
break
|
|
681
|
+
self._insert_chunk(type_name, sql, chunk)
|
|
682
|
+
n += len(chunk)
|
|
683
|
+
if not was_active and every > 0 and n % every == 0:
|
|
684
|
+
self.commit()
|
|
685
|
+
self.begin()
|
|
686
|
+
if not was_active:
|
|
687
|
+
self.commit()
|
|
688
|
+
return n
|
|
689
|
+
except BaseException:
|
|
690
|
+
if not was_active:
|
|
691
|
+
try:
|
|
692
|
+
if self.is_transaction_active():
|
|
693
|
+
self.rollback()
|
|
694
|
+
except Exception: # nosec B110 - best-effort rollback
|
|
695
|
+
pass
|
|
696
|
+
raise
|
|
697
|
+
|
|
698
|
+
def _insert_chunk(self, type_name: str, sql: str, chunk) -> None:
|
|
699
|
+
if lib.INSERT_ROWS:
|
|
700
|
+
try:
|
|
701
|
+
payload = json_bulk_dumps(chunk)
|
|
702
|
+
except (TypeError, ValueError):
|
|
703
|
+
payload = None
|
|
704
|
+
if payload is not None:
|
|
705
|
+
reply = json.loads(
|
|
706
|
+
lib.call(
|
|
707
|
+
"arcadedb_n_insert_rows",
|
|
708
|
+
self._h,
|
|
709
|
+
lib.b(type_name),
|
|
710
|
+
lib.b(payload),
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
if not reply.get("ok"):
|
|
714
|
+
raise _error(f"Failed to bulk-insert into '{type_name}'", reply)
|
|
715
|
+
return
|
|
716
|
+
if lib.EXEC_MANY:
|
|
717
|
+
# values JSON text cannot carry unchanged (a date-time, a decimal, NaN, a big integer): the same statement per
|
|
718
|
+
# row, but all rows of the chunk in one crossing (arcadedb_n_exec_many), not one call per row
|
|
719
|
+
self._command_many("sql", sql, [{"row": row} for row in chunk])
|
|
720
|
+
return
|
|
721
|
+
for row in chunk:
|
|
722
|
+
self.command("sql", sql, {"row": row}).close()
|
|
723
|
+
|
|
724
|
+
def _command_many(self, language: str, command: str, rows) -> int:
|
|
725
|
+
"""Experimental (phase 8): the statement once per row of ``rows`` (each a list of positional values or a dict of named values),
|
|
726
|
+
all in one crossing and in the open transaction; the cursors are drained and closed. Not part of the public API.
|
|
727
|
+
"""
|
|
728
|
+
self._check_not_closed()
|
|
729
|
+
payload = json.dumps([_prep_row(r) for r in rows], default=codec_default)
|
|
730
|
+
reply = json.loads(
|
|
731
|
+
lib.call(
|
|
732
|
+
"arcadedb_n_exec_many",
|
|
733
|
+
self._h,
|
|
734
|
+
lib.b(language),
|
|
735
|
+
lib.b(command),
|
|
736
|
+
lib.b(payload),
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
if not reply.get("ok"):
|
|
740
|
+
raise _error("Command failed", reply)
|
|
741
|
+
return reply["n"]
|
|
742
|
+
|
|
743
|
+
def _parallel_load_failed(self, type_name, error) -> ArcadeDBError:
|
|
744
|
+
"""The error for a parallel load that failed while handing rows to the writers: waits for the queue to drain first, so
|
|
745
|
+
the caller sees a final state, and says that the rows handed over before the failure may have been stored.
|
|
746
|
+
"""
|
|
747
|
+
try:
|
|
748
|
+
self._async().wait_completion()
|
|
749
|
+
except Exception: # nosec B110 - the original error is the one to report
|
|
750
|
+
pass
|
|
751
|
+
text = str(error)
|
|
752
|
+
prefix = "Failed to bulk-insert: "
|
|
753
|
+
if text.startswith(prefix):
|
|
754
|
+
text = text[len(prefix) :]
|
|
755
|
+
return ArcadeDBError(
|
|
756
|
+
f"Failed to bulk-insert into '{type_name}': {text}; the rows handed to the "
|
|
757
|
+
f"parallel writers before the failure may have been stored (the call waited "
|
|
758
|
+
f"for them before raising)"
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
def _insert_many_parallel(self, type_name, rows) -> int:
|
|
762
|
+
"""insert_many(parallel=True): chunks go to the async writers; the failure count is read after the final wait."""
|
|
763
|
+
import itertools
|
|
764
|
+
|
|
765
|
+
from . import extras
|
|
766
|
+
|
|
767
|
+
iterator = iter(rows)
|
|
768
|
+
tokens, n = [], 0
|
|
769
|
+
try:
|
|
770
|
+
while True:
|
|
771
|
+
chunk = list(itertools.islice(iterator, 10_000))
|
|
772
|
+
if not chunk:
|
|
773
|
+
break
|
|
774
|
+
try:
|
|
775
|
+
tokens.append(extras.insert_parallel(self, type_name, chunk)["fid"])
|
|
776
|
+
except ArcadeDBError as e:
|
|
777
|
+
# as the JPype path: the rows handed to the writers before the failure are written whatever this call does
|
|
778
|
+
raise self._parallel_load_failed(type_name, e) from e
|
|
779
|
+
n += len(chunk)
|
|
780
|
+
except ArcadeDBError:
|
|
781
|
+
raise
|
|
782
|
+
except BaseException:
|
|
783
|
+
try:
|
|
784
|
+
self._async().wait_completion()
|
|
785
|
+
except Exception: # nosec B110 - the original error is the one to report
|
|
786
|
+
pass
|
|
787
|
+
raise
|
|
788
|
+
try:
|
|
789
|
+
self._async().wait_completion()
|
|
790
|
+
except Exception as e:
|
|
791
|
+
raise ArcadeDBError(f"Failed to bulk-insert into '{type_name}': {e}") from e
|
|
792
|
+
n_failed, first = 0, None
|
|
793
|
+
for token in tokens:
|
|
794
|
+
result = extras.failures(self, token)
|
|
795
|
+
if result["count"] and first is None:
|
|
796
|
+
first = result["first"]
|
|
797
|
+
n_failed += result["count"]
|
|
798
|
+
if n_failed:
|
|
799
|
+
raise ArcadeDBError(
|
|
800
|
+
f"Failed to bulk-insert into '{type_name}': the parallel writers reported {n_failed} failed record(s) "
|
|
801
|
+
f"of {n}; the rest may have been stored (first failure: {first})"
|
|
802
|
+
)
|
|
803
|
+
return n
|
|
804
|
+
|
|
805
|
+
def _kind_of(self, type_name):
|
|
806
|
+
"""'v', 'e' or 'd' for a type (cached): which wrapper a record of that type gets."""
|
|
807
|
+
kinds = self.__dict__.setdefault("_type_kinds", {})
|
|
808
|
+
if type_name not in kinds:
|
|
809
|
+
row = self.query(
|
|
810
|
+
"sql", "SELECT type FROM schema:types WHERE name = ?", type_name
|
|
811
|
+
).first()
|
|
812
|
+
kinds[type_name] = {"vertex": "v", "edge": "e"}.get(
|
|
813
|
+
row.get("type") if row else None, "d"
|
|
814
|
+
)
|
|
815
|
+
return kinds[type_name]
|
|
816
|
+
|
|
817
|
+
def _async(self):
|
|
818
|
+
from .extras import NativeAsyncExecutor
|
|
819
|
+
|
|
820
|
+
return NativeAsyncExecutor(self)
|
|
821
|
+
|
|
822
|
+
# -- not on native yet
|
|
823
|
+
|
|
824
|
+
def get_java_database(self):
|
|
825
|
+
not_yet("get_java_database")
|
|
826
|
+
|
|
827
|
+
def new_vertex(self, type_name: str):
|
|
828
|
+
"""A vertex that is created by ``save()`` (``CREATE VERTEX .. CONTENT``)."""
|
|
829
|
+
from .records import Vertex
|
|
830
|
+
|
|
831
|
+
self._check_not_closed()
|
|
832
|
+
return Vertex(self, type_name)
|
|
833
|
+
|
|
834
|
+
def new_document(self, type_name: str):
|
|
835
|
+
"""A document that is created by ``save()`` (``INSERT INTO .. CONTENT``)."""
|
|
836
|
+
from .records import Document
|
|
837
|
+
|
|
838
|
+
self._check_not_closed()
|
|
839
|
+
return Document(self, type_name)
|
|
840
|
+
|
|
841
|
+
def graph_batch(self, **options):
|
|
842
|
+
"""A GraphBatch for bulk graph loads (keywords as :meth:`Database.graph_batch`)."""
|
|
843
|
+
from .extras import NativeGraphBatch
|
|
844
|
+
|
|
845
|
+
self._check_not_closed()
|
|
846
|
+
return NativeGraphBatch.create(self, **options)
|
|
847
|
+
|
|
848
|
+
def async_executor(self):
|
|
849
|
+
"""The database's async executor."""
|
|
850
|
+
from .extras import NativeAsyncExecutor
|
|
851
|
+
|
|
852
|
+
self._check_not_closed()
|
|
853
|
+
executor = NativeAsyncExecutor(self)
|
|
854
|
+
self._async_executors.append(executor)
|
|
855
|
+
return executor
|
|
856
|
+
|
|
857
|
+
def register_function(self, name, fn, min_args=0, max_args=-1):
|
|
858
|
+
"""Make a Python callable available to SQL (process-wide, see ``extras.register_function``)."""
|
|
859
|
+
from .extras import register_function
|
|
860
|
+
|
|
861
|
+
register_function(name, fn, min_args, max_args)
|
|
862
|
+
|
|
863
|
+
@property
|
|
864
|
+
def schema(self):
|
|
865
|
+
"""DDL helpers over SQL statements (the DSL-first subset); everything else is not on native yet."""
|
|
866
|
+
self._check_not_closed()
|
|
867
|
+
return NativeSchema(self)
|
|
868
|
+
|
|
869
|
+
def create_vector_index(
|
|
870
|
+
self,
|
|
871
|
+
vertex_type,
|
|
872
|
+
vector_property,
|
|
873
|
+
dimensions,
|
|
874
|
+
id_property=None,
|
|
875
|
+
distance_function="cosine",
|
|
876
|
+
max_connections=32,
|
|
877
|
+
beam_width=100,
|
|
878
|
+
quantization="INT8",
|
|
879
|
+
encoding=None,
|
|
880
|
+
location_cache_size=None,
|
|
881
|
+
graph_build_cache_size=None,
|
|
882
|
+
mutations_before_rebuild=None,
|
|
883
|
+
store_vectors_in_graph=False,
|
|
884
|
+
add_hierarchy=True,
|
|
885
|
+
pq_subspaces=None,
|
|
886
|
+
pq_clusters=None,
|
|
887
|
+
pq_center_globally=None,
|
|
888
|
+
pq_training_limit=None,
|
|
889
|
+
build_graph_now=True,
|
|
890
|
+
):
|
|
891
|
+
"""A vector index (``CREATE INDEX .. LSM_VECTOR``) and its :class:`VectorIndex` wrapper; arguments as the JPype path."""
|
|
892
|
+
from .schema_objects import create_vector_index
|
|
893
|
+
|
|
894
|
+
self._check_not_closed()
|
|
895
|
+
if location_cache_size is not None:
|
|
896
|
+
raise ValueError(
|
|
897
|
+
"location_cache_size is no longer supported (ArcadeDB issues #5559 and #5568): a vector location is the only "
|
|
898
|
+
"mapping from a vector id to its record, so capping the location index drops vectors from searches and from "
|
|
899
|
+
"countEntries() instead of spilling them to disk. Remove the argument and size the heap for the live vector "
|
|
900
|
+
"set instead."
|
|
901
|
+
)
|
|
902
|
+
return create_vector_index(
|
|
903
|
+
self,
|
|
904
|
+
vertex_type,
|
|
905
|
+
vector_property,
|
|
906
|
+
dimensions,
|
|
907
|
+
id_property,
|
|
908
|
+
distance_function,
|
|
909
|
+
max_connections,
|
|
910
|
+
beam_width,
|
|
911
|
+
quantization,
|
|
912
|
+
encoding,
|
|
913
|
+
graph_build_cache_size,
|
|
914
|
+
mutations_before_rebuild,
|
|
915
|
+
store_vectors_in_graph,
|
|
916
|
+
add_hierarchy,
|
|
917
|
+
pq_subspaces,
|
|
918
|
+
pq_clusters,
|
|
919
|
+
pq_center_globally,
|
|
920
|
+
pq_training_limit,
|
|
921
|
+
build_graph_now,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def import_documents(
|
|
925
|
+
self,
|
|
926
|
+
source,
|
|
927
|
+
document_type: str = "Document",
|
|
928
|
+
*,
|
|
929
|
+
file_type=None,
|
|
930
|
+
delimiter=None,
|
|
931
|
+
header=None,
|
|
932
|
+
skip_entries=None,
|
|
933
|
+
properties_include=None,
|
|
934
|
+
commit_every=None,
|
|
935
|
+
parallel=None,
|
|
936
|
+
wal=None,
|
|
937
|
+
verbose_level=None,
|
|
938
|
+
probe_only=None,
|
|
939
|
+
force_database_create=None,
|
|
940
|
+
trim_text=None,
|
|
941
|
+
on_row_error=None,
|
|
942
|
+
extra_settings=None,
|
|
943
|
+
):
|
|
944
|
+
"""Import document-shaped files (CSV, JSON, ...) through the engine's importer; same arguments as the JPype path."""
|
|
945
|
+
from ..importer import (
|
|
946
|
+
ImportResult,
|
|
947
|
+
_build_import_settings,
|
|
948
|
+
normalize_import_source,
|
|
949
|
+
)
|
|
950
|
+
from . import extras
|
|
951
|
+
|
|
952
|
+
self._check_not_closed()
|
|
953
|
+
source_url = normalize_import_source(source)
|
|
954
|
+
settings = _build_import_settings(
|
|
955
|
+
source_url,
|
|
956
|
+
document_type=document_type,
|
|
957
|
+
file_type=file_type,
|
|
958
|
+
delimiter=delimiter,
|
|
959
|
+
header=header,
|
|
960
|
+
skip_entries=skip_entries,
|
|
961
|
+
properties_include=properties_include,
|
|
962
|
+
commit_every=commit_every,
|
|
963
|
+
parallel=parallel,
|
|
964
|
+
wal=wal,
|
|
965
|
+
verbose_level=verbose_level,
|
|
966
|
+
probe_only=probe_only,
|
|
967
|
+
force_database_create=force_database_create,
|
|
968
|
+
trim_text=trim_text,
|
|
969
|
+
on_row_error=on_row_error,
|
|
970
|
+
extra_settings=extra_settings,
|
|
971
|
+
)
|
|
972
|
+
try:
|
|
973
|
+
stats = extras.run_import(self, settings, parallel, commit_every, wal)
|
|
974
|
+
return ImportResult(
|
|
975
|
+
result="PROBE_ONLY" if probe_only and stats is None else "OK",
|
|
976
|
+
operation="import documents",
|
|
977
|
+
source_url=source_url,
|
|
978
|
+
statistics=stats or {},
|
|
979
|
+
)
|
|
980
|
+
except Exception as e:
|
|
981
|
+
raise ArcadeDBError(f"Document import failed: {e}") from e
|
|
982
|
+
|
|
983
|
+
def export_database(
|
|
984
|
+
self,
|
|
985
|
+
file_path,
|
|
986
|
+
format: str = "jsonl",
|
|
987
|
+
overwrite: bool = False,
|
|
988
|
+
include_types=None,
|
|
989
|
+
exclude_types=None,
|
|
990
|
+
verbose: int = 1,
|
|
991
|
+
) -> dict:
|
|
992
|
+
self._check_not_closed()
|
|
993
|
+
from ..exporter import export_database
|
|
994
|
+
|
|
995
|
+
return export_database(
|
|
996
|
+
self, file_path, format, overwrite, include_types, exclude_types, verbose
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def export_to_csv(
|
|
1000
|
+
self, query: str, file_path: str, language: str = "sql", fieldnames=None
|
|
1001
|
+
):
|
|
1002
|
+
self._check_not_closed()
|
|
1003
|
+
from ..exporter import export_to_csv
|
|
1004
|
+
|
|
1005
|
+
export_to_csv(self.query(language, query), file_path, fieldnames)
|
|
1006
|
+
|
|
1007
|
+
def insert_columns(
|
|
1008
|
+
self, type_name, columns, commit_every: int = 10_000, parallel: bool = False
|
|
1009
|
+
) -> int:
|
|
1010
|
+
"""Bulk insert from whole columns (dict of name -> numpy array or list), as rows through :meth:`insert_many`."""
|
|
1011
|
+
self._check_not_closed()
|
|
1012
|
+
items = list(columns.items()) if hasattr(columns, "items") else None
|
|
1013
|
+
if not items:
|
|
1014
|
+
raise ValueError("insert_columns needs at least one column")
|
|
1015
|
+
for name, values in items:
|
|
1016
|
+
if not isinstance(name, str):
|
|
1017
|
+
raise ValueError(f"column names must be strings, got {name!r}")
|
|
1018
|
+
if isinstance(values, (str, bytes, bytearray)):
|
|
1019
|
+
raise TypeError(
|
|
1020
|
+
f"column {name!r} is a {type(values).__name__}, not a sequence of values; "
|
|
1021
|
+
f"wrap a single value in a list"
|
|
1022
|
+
)
|
|
1023
|
+
known = {n: len(v) for n, v in items if hasattr(v, "__len__")}
|
|
1024
|
+
if len(set(known.values())) > 1:
|
|
1025
|
+
raise ValueError(f"columns differ in length: {known}")
|
|
1026
|
+
if not parallel:
|
|
1027
|
+
from .bulk import insert_columns_fast
|
|
1028
|
+
|
|
1029
|
+
done = insert_columns_fast(self, type_name, columns, commit_every)
|
|
1030
|
+
if done is not None:
|
|
1031
|
+
return done
|
|
1032
|
+
names = [n for n, _ in items]
|
|
1033
|
+
data = [_column_values(n, v) for n, v in items]
|
|
1034
|
+
lengths = {n: len(c) for n, c in zip(names, data)}
|
|
1035
|
+
if len(set(lengths.values())) != 1:
|
|
1036
|
+
raise ValueError(f"columns differ in length: {lengths}")
|
|
1037
|
+
if not data[0]:
|
|
1038
|
+
return 0
|
|
1039
|
+
rows = (dict(zip(names, vals)) for vals in zip(*data))
|
|
1040
|
+
return self.insert_many(type_name, rows, commit_every, parallel)
|
|
1041
|
+
|
|
1042
|
+
def insert_dataframe(
|
|
1043
|
+
self, type_name, df, *, batch_size: int = 10_000, parallel: bool = False
|
|
1044
|
+
) -> int:
|
|
1045
|
+
"""A pandas or Polars DataFrame into a type, in batches: the strict Arrow loading of ``Database.insert_dataframe``
|
|
1046
|
+
(dtype refusals, declared types, NaN rules, the committed-rows message), batches through :meth:`insert_many` for a
|
|
1047
|
+
document type and ``GraphBatch.create_vertices`` for a vertex type (``arcadedb_native/dataframe.py``).
|
|
1048
|
+
"""
|
|
1049
|
+
from ..dataframe import insert_table, to_arrow_table
|
|
1050
|
+
|
|
1051
|
+
self._check_not_closed()
|
|
1052
|
+
table = to_arrow_table(df, what="insert_dataframe()")
|
|
1053
|
+
return insert_table(
|
|
1054
|
+
self,
|
|
1055
|
+
type_name,
|
|
1056
|
+
table,
|
|
1057
|
+
batch_size=batch_size,
|
|
1058
|
+
parallel=parallel,
|
|
1059
|
+
what="insert_dataframe()",
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
def insert_arrow(
|
|
1063
|
+
self, type_name, table, *, batch_size: int = 10_000, parallel: bool = False
|
|
1064
|
+
) -> int:
|
|
1065
|
+
"""An Arrow table (or ``RecordBatch``, ``RecordBatchReader``, Arrow PyCapsule object) into a type, in batches; as
|
|
1066
|
+
:meth:`insert_dataframe`.
|
|
1067
|
+
"""
|
|
1068
|
+
from ..dataframe import insert_table, to_arrow_table
|
|
1069
|
+
|
|
1070
|
+
self._check_not_closed()
|
|
1071
|
+
arrow_table = to_arrow_table(table, what="insert_arrow()")
|
|
1072
|
+
return insert_table(
|
|
1073
|
+
self,
|
|
1074
|
+
type_name,
|
|
1075
|
+
arrow_table,
|
|
1076
|
+
batch_size=batch_size,
|
|
1077
|
+
parallel=parallel,
|
|
1078
|
+
what="insert_arrow()",
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
def set_wal_flush(self, mode: str):
|
|
1082
|
+
self._check_not_closed()
|
|
1083
|
+
valid = {"no": "NO", "yes_nometadata": "YES_NOMETADATA", "yes_full": "YES_FULL"}
|
|
1084
|
+
if mode not in valid:
|
|
1085
|
+
raise ValueError(
|
|
1086
|
+
f"Invalid WAL flush mode: {mode}. Must be one of: {list(valid)}"
|
|
1087
|
+
)
|
|
1088
|
+
from .extras import db_set
|
|
1089
|
+
|
|
1090
|
+
try:
|
|
1091
|
+
db_set(self, "wal_flush", valid[mode])
|
|
1092
|
+
except ArcadeDBError as e:
|
|
1093
|
+
raise ArcadeDBError(f"Failed to set WAL flush mode: {e}") from e
|
|
1094
|
+
|
|
1095
|
+
def set_read_your_writes(self, enabled: bool):
|
|
1096
|
+
self._check_not_closed()
|
|
1097
|
+
from .extras import db_set
|
|
1098
|
+
|
|
1099
|
+
db_set(self, "read_your_writes", "true" if enabled else "false")
|
|
1100
|
+
|
|
1101
|
+
def is_read_your_writes(self) -> bool:
|
|
1102
|
+
self._check_not_closed()
|
|
1103
|
+
from .extras import db_set
|
|
1104
|
+
|
|
1105
|
+
return bool(db_set(self, "read_your_writes")["v"])
|
|
1106
|
+
|
|
1107
|
+
def set_auto_transaction(self, enabled: bool):
|
|
1108
|
+
self._check_not_closed()
|
|
1109
|
+
from .extras import db_set
|
|
1110
|
+
|
|
1111
|
+
db_set(self, "auto_transaction", "true" if enabled else "false")
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _column_values(name, values):
|
|
1115
|
+
"""One ``insert_columns`` column as a list of Python values, with the JPype path's refusals (a dtype that does not
|
|
1116
|
+
cross, a value beyond the 64-bit signed range, a column that is not one-dimensional).
|
|
1117
|
+
"""
|
|
1118
|
+
try:
|
|
1119
|
+
import numpy as np
|
|
1120
|
+
except ImportError: # pragma: no cover
|
|
1121
|
+
np = None
|
|
1122
|
+
if hasattr(values, "to_numpy") and not isinstance(values, (list, tuple)):
|
|
1123
|
+
dtype = getattr(values, "dtype", None)
|
|
1124
|
+
if np is not None and isinstance(dtype, np.dtype):
|
|
1125
|
+
values = values.to_numpy()
|
|
1126
|
+
else: # a pandas extension dtype (Int64, string, boolean, category): its <NA> is a null
|
|
1127
|
+
values = values.to_numpy(dtype=object, na_value=None)
|
|
1128
|
+
if np is not None and isinstance(values, np.ndarray):
|
|
1129
|
+
if values.ndim != 1:
|
|
1130
|
+
raise ValueError(
|
|
1131
|
+
f"column {name!r} is {values.ndim}-dimensional; a column is one-dimensional"
|
|
1132
|
+
)
|
|
1133
|
+
kind = values.dtype.kind
|
|
1134
|
+
if kind == "u" and values.size and int(values.max()) > np.iinfo(np.int64).max:
|
|
1135
|
+
raise ValueError(
|
|
1136
|
+
f"column {name!r} holds a value beyond the 64-bit signed range"
|
|
1137
|
+
)
|
|
1138
|
+
if kind in "MmcV":
|
|
1139
|
+
raise TypeError(
|
|
1140
|
+
f"column {name!r} has dtype {values.dtype}, which does not cross natively; "
|
|
1141
|
+
"convert it to Python values or use insert_many"
|
|
1142
|
+
)
|
|
1143
|
+
return values.tolist()
|
|
1144
|
+
return list(values)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
class _JavaObject:
|
|
1148
|
+
"""What the JPype Schema returns where the native one has no Java object: any use says so."""
|
|
1149
|
+
|
|
1150
|
+
def __bool__(self):
|
|
1151
|
+
return True
|
|
1152
|
+
|
|
1153
|
+
def __getattr__(self, name):
|
|
1154
|
+
not_yet(f"the Java object a Schema method returns ({name})")
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
class NativeSchema:
|
|
1158
|
+
"""Type, property and index creation as SQL statements. Returns None where the JPype Schema returns a Java object."""
|
|
1159
|
+
|
|
1160
|
+
def __init__(self, db):
|
|
1161
|
+
self._db = db
|
|
1162
|
+
|
|
1163
|
+
def _run(self, sql):
|
|
1164
|
+
self._db.command("sql", sql).close()
|
|
1165
|
+
|
|
1166
|
+
def _type(self, kind, name, buckets, if_not_exists=False):
|
|
1167
|
+
ine = " IF NOT EXISTS" if if_not_exists else ""
|
|
1168
|
+
suffix = f" BUCKETS {int(buckets)}" if buckets else ""
|
|
1169
|
+
self._run(f"CREATE {kind} TYPE `{name}`{ine}{suffix}")
|
|
1170
|
+
return _JavaObject()
|
|
1171
|
+
|
|
1172
|
+
def create_document_type(self, name, buckets=None):
|
|
1173
|
+
return self._type("DOCUMENT", name, buckets)
|
|
1174
|
+
|
|
1175
|
+
def create_vertex_type(self, name, buckets=None):
|
|
1176
|
+
return self._type("VERTEX", name, buckets)
|
|
1177
|
+
|
|
1178
|
+
def create_edge_type(self, name, buckets=None):
|
|
1179
|
+
return self._type("EDGE", name, buckets)
|
|
1180
|
+
|
|
1181
|
+
def get_or_create_document_type(self, name, buckets=None):
|
|
1182
|
+
return self._type("DOCUMENT", name, buckets, True)
|
|
1183
|
+
|
|
1184
|
+
def get_or_create_vertex_type(self, name, buckets=None):
|
|
1185
|
+
return self._type("VERTEX", name, buckets, True)
|
|
1186
|
+
|
|
1187
|
+
def get_or_create_edge_type(self, name, buckets=None):
|
|
1188
|
+
return self._type("EDGE", name, buckets, True)
|
|
1189
|
+
|
|
1190
|
+
def exists_type(self, name):
|
|
1191
|
+
rows = self._db.query(
|
|
1192
|
+
"sql", "SELECT FROM schema:types WHERE name = ?", name
|
|
1193
|
+
).to_list()
|
|
1194
|
+
return bool(rows)
|
|
1195
|
+
|
|
1196
|
+
def drop_type(self, name):
|
|
1197
|
+
self._run(f"DROP TYPE `{name}`")
|
|
1198
|
+
|
|
1199
|
+
def create_property(self, type_name, property_name, property_type, of_type=None):
|
|
1200
|
+
t = getattr(property_type, "value", property_type)
|
|
1201
|
+
o = f" OF {getattr(of_type, 'value', of_type)}" if of_type else ""
|
|
1202
|
+
self._run(f"CREATE PROPERTY `{type_name}`.`{property_name}` {t}{o}")
|
|
1203
|
+
return _JavaObject()
|
|
1204
|
+
|
|
1205
|
+
def get_or_create_property(
|
|
1206
|
+
self, type_name, property_name, property_type, of_type=None
|
|
1207
|
+
):
|
|
1208
|
+
t = getattr(property_type, "value", property_type)
|
|
1209
|
+
o = f" OF {getattr(of_type, 'value', of_type)}" if of_type else ""
|
|
1210
|
+
self._run(
|
|
1211
|
+
f"CREATE PROPERTY `{type_name}`.`{property_name}` IF NOT EXISTS {t}{o}"
|
|
1212
|
+
)
|
|
1213
|
+
return _JavaObject()
|
|
1214
|
+
|
|
1215
|
+
def drop_property(self, type_name, property_name):
|
|
1216
|
+
self._run(f"DROP PROPERTY `{type_name}`.`{property_name}`")
|
|
1217
|
+
|
|
1218
|
+
_INDEX_KIND = {
|
|
1219
|
+
("LSM_TREE", True): "UNIQUE",
|
|
1220
|
+
("LSM_TREE", False): "NOTUNIQUE",
|
|
1221
|
+
("HASH", True): "UNIQUE_HASH",
|
|
1222
|
+
("HASH", False): "NOTUNIQUE_HASH",
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
def create_index(
|
|
1226
|
+
self, type_name, property_names, unique=False, index_type="LSM_TREE"
|
|
1227
|
+
):
|
|
1228
|
+
kind = getattr(index_type, "value", index_type)
|
|
1229
|
+
kind = self._INDEX_KIND.get((kind, bool(unique)), kind)
|
|
1230
|
+
cols = ", ".join(f"`{p}`" for p in property_names)
|
|
1231
|
+
self._run(f"CREATE INDEX ON `{type_name}` ({cols}) {kind}")
|
|
1232
|
+
return _JavaObject()
|
|
1233
|
+
|
|
1234
|
+
def exists_index(self, index_name):
|
|
1235
|
+
return bool(
|
|
1236
|
+
self._db.query(
|
|
1237
|
+
"sql", "SELECT FROM schema:indexes WHERE name = ?", index_name
|
|
1238
|
+
).to_list()
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
def drop_index(self, index_name, force=False):
|
|
1242
|
+
self._run(f"DROP INDEX `{index_name}`")
|
|
1243
|
+
|
|
1244
|
+
# -- reads, as small objects with the engine's accessors (schema_objects.py)
|
|
1245
|
+
|
|
1246
|
+
def get_type(self, name):
|
|
1247
|
+
from .schema_objects import NativeType
|
|
1248
|
+
|
|
1249
|
+
rows = self._db.query(
|
|
1250
|
+
"sql", "SELECT FROM schema:types WHERE name = ?", name
|
|
1251
|
+
).to_list()
|
|
1252
|
+
return NativeType(rows[0]) if rows else None
|
|
1253
|
+
|
|
1254
|
+
def get_types(self):
|
|
1255
|
+
from .schema_objects import NativeType
|
|
1256
|
+
|
|
1257
|
+
return [
|
|
1258
|
+
NativeType(r)
|
|
1259
|
+
for r in self._db.query("sql", "SELECT FROM schema:types").to_list()
|
|
1260
|
+
]
|
|
1261
|
+
|
|
1262
|
+
def get_indexes(self):
|
|
1263
|
+
from .schema_objects import NativeIndex
|
|
1264
|
+
|
|
1265
|
+
return [
|
|
1266
|
+
NativeIndex(r)
|
|
1267
|
+
for r in self._db.query("sql", "SELECT FROM schema:indexes").to_list()
|
|
1268
|
+
]
|
|
1269
|
+
|
|
1270
|
+
def get_index_by_name(self, index_name):
|
|
1271
|
+
from .schema_objects import NativeIndex
|
|
1272
|
+
|
|
1273
|
+
rows = self._db.query(
|
|
1274
|
+
"sql", "SELECT FROM schema:indexes WHERE name = ?", index_name
|
|
1275
|
+
).to_list()
|
|
1276
|
+
return NativeIndex(rows[0]) if rows else None
|
|
1277
|
+
|
|
1278
|
+
def get_or_create_index(
|
|
1279
|
+
self, type_name, property_names, unique=False, index_type="LSM_TREE"
|
|
1280
|
+
):
|
|
1281
|
+
kind = getattr(index_type, "value", index_type)
|
|
1282
|
+
kind = self._INDEX_KIND.get((kind, bool(unique)), kind)
|
|
1283
|
+
cols = ", ".join(f"`{p}`" for p in property_names)
|
|
1284
|
+
try:
|
|
1285
|
+
self._run(f"CREATE INDEX IF NOT EXISTS ON `{type_name}` ({cols}) {kind}")
|
|
1286
|
+
except ArcadeDBError as e:
|
|
1287
|
+
raise ArcadeDBError(
|
|
1288
|
+
f"Failed to get/create index on '{type_name}[{','.join(property_names)}]': {e}"
|
|
1289
|
+
) from e
|
|
1290
|
+
name = f"{type_name}[{','.join(property_names)}]"
|
|
1291
|
+
return self.get_index_by_name(name) or _JavaObject()
|
|
1292
|
+
|
|
1293
|
+
def _vector_rows(self):
|
|
1294
|
+
# the type-level rows (named Type[prop]); the per-bucket rows carry a file id
|
|
1295
|
+
return [
|
|
1296
|
+
r
|
|
1297
|
+
for r in self._db.query(
|
|
1298
|
+
"sql", "SELECT FROM schema:indexes WHERE indexType = 'LSM_VECTOR'"
|
|
1299
|
+
).to_list()
|
|
1300
|
+
if "[" in r["name"] and "fileId" not in r
|
|
1301
|
+
]
|
|
1302
|
+
|
|
1303
|
+
def get_vector_index(self, vertex_type, vector_property):
|
|
1304
|
+
from .schema_objects import NativeVectorIndex
|
|
1305
|
+
|
|
1306
|
+
for r in self._vector_rows():
|
|
1307
|
+
if r["typeName"] == vertex_type and r["properties"] == [[vector_property]]:
|
|
1308
|
+
return NativeVectorIndex(
|
|
1309
|
+
self._db, vertex_type, vector_property, r["name"]
|
|
1310
|
+
)
|
|
1311
|
+
return None
|
|
1312
|
+
|
|
1313
|
+
def list_vector_indexes(self):
|
|
1314
|
+
return [r["name"] for r in self._vector_rows()]
|
|
1315
|
+
|
|
1316
|
+
def __getattr__(self, name):
|
|
1317
|
+
if name.startswith("_"):
|
|
1318
|
+
raise AttributeError(name)
|
|
1319
|
+
not_yet(f"Schema.{name} (returns Java objects)")
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
class NativeDatabaseFactory:
|
|
1323
|
+
"""Create/open databases by path."""
|
|
1324
|
+
|
|
1325
|
+
def __init__(self, path: str, jvm_kwargs: Optional[dict] = None):
|
|
1326
|
+
if jvm_kwargs:
|
|
1327
|
+
import logging
|
|
1328
|
+
|
|
1329
|
+
logging.getLogger("arcadedb_native").warning(
|
|
1330
|
+
"jvm_kwargs %s ignored: the native backend has no JVM",
|
|
1331
|
+
sorted(jvm_kwargs),
|
|
1332
|
+
)
|
|
1333
|
+
self._path = os.fspath(path)
|
|
1334
|
+
|
|
1335
|
+
def create(self) -> NativeDatabase:
|
|
1336
|
+
return NativeDatabase(
|
|
1337
|
+
_call("Failed to create database", "arcadedb_n_open", lib.b(self._path), 0)[
|
|
1338
|
+
"h"
|
|
1339
|
+
]
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
def open(self, read_only: bool = False) -> NativeDatabase:
|
|
1343
|
+
mode = 2 if read_only else 1
|
|
1344
|
+
return NativeDatabase(
|
|
1345
|
+
_call(
|
|
1346
|
+
"Failed to open database", "arcadedb_n_open", lib.b(self._path), mode
|
|
1347
|
+
)["h"]
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
def exists(self) -> bool:
|
|
1351
|
+
return _call(
|
|
1352
|
+
"Failed to check if database exists",
|
|
1353
|
+
"arcadedb_n_open",
|
|
1354
|
+
lib.b(self._path),
|
|
1355
|
+
3,
|
|
1356
|
+
)["exists"]
|