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,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"]