Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/arcadedb/_native/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ def _default(value, _bin=None):
_PLAIN = frozenset((str, int, float, bool, type(None)))
_FLOAT_ONLY = frozenset((float,))
_dumps = json.dumps
# the C function that json.dumps writes a str with
_enc_str = json.encoder.encode_basestring_ascii
_fast = os.environ.get("ARCADEDB_NATIVE_FASTENC", "1") != "0"


Expand Down Expand Up @@ -159,6 +161,17 @@ def encode_params(args, binary=False):
return ("", None, 0) if binary else ""
values = args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
plain = _PLAIN
if _fast and len(values) == 1:
# one int or str parameter (the point read, WHERE id = ?): the text is written directly, the bytes json.dumps({"pos": [v]}) gives,
# without the set of types and the encoder call. Only the exact types: a bool, an int subclass or a float takes the general path
v = values[0]
t = type(v)
if t is int:
text = '{"pos": [%d]}' % v
return (text, None, 0) if binary else text
if t is str:
text = '{"pos": [%s]}' % _enc_str(v)
return (text, None, 0) if binary else text
if _fast and set(map(type, values)) <= plain:
# all plain scalars (the per-row INSERT of a load): the cached default encoder, no default hook, no per-call encoder object
text = _dumps({"pos": values if type(values) is list else list(values)})
Expand Down
24 changes: 17 additions & 7 deletions src/arcadedb/_native/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
_FIRST_BATCH = 256
_EPOCH_DAY_ORDINAL = date(1970, 1, 1).toordinal()
_NEXT_BATCH = 2048
_wrap_record = None
_KIND = {"v": "v", "e": "e", "d": "d"}


Expand Down Expand Up @@ -101,12 +102,14 @@ def get_rid(self) -> Optional[str]:
return None if rid is None else str(rid)

def _record(self, kinds):
from .records import _wrap

global _wrap_record
# records.py imports this module: looked up on first use, then kept (an import statement per call costs about 0.4 us)
if _wrap_record is None:
from .records import _wrap as _wrap_record
self._check_open()
if not self._meta or self._meta[2] not in kinds:
return None
return _wrap(self._database, self)
return _wrap_record(self._database, self)

def get_vertex(self):
return self._record("v")
Expand Down Expand Up @@ -360,6 +363,14 @@ def count(self) -> int:
return n

def first(self) -> Optional[NativeResult]:
buf = self._buf
database = self._database
if buf and not self._closed and (database is None or not database._closed):
# the usual point read: the first row is already here and the result is readable (what __next__ checks): take it and close
props, meta = buf.popleft()
row = NativeResult(props, meta, database)
self.close()
return row
try:
return next(iter(self))
except StopIteration:
Expand Down Expand Up @@ -532,12 +543,11 @@ def _exec(
count,
)
else:
raw, floats = lib.call_bin(
"arcadedb_n_exec",
raw, floats = lib.exec_query(
self._h,
flags,
lib.b(language),
lib.b(command),
lib.bc(language),
lib.bc(command),
lib.b(text),
_FIRST_BATCH,
)
Expand Down
26 changes: 26 additions & 0 deletions src/arcadedb/_native/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,32 @@ def alloc(size: int):
return lib.arcadedb_n_alloc(_thread_handle(), size)


def exec_query(handle, flags, language, command, params, count):
"""``arcadedb_n_exec`` for a statement whose parameters need no float side buffer: the same ``(text, floats)`` as :func:`call_bin`
returns for it (keep the two reply readers in step). The entry point is not looked up by name per call, the isolate thread is read
without a function call and no argument tuple is built.
"""
lib = _lib or _load()
t = _local.__dict__.get("thread")
th = t.handle if t is not None else _thread_handle()
ptr = lib.arcadedb_n_exec(th, handle, flags, language, command, params, count)
try:
raw = ctypes.string_at(ptr)
floats = None
# a negative start counts from the end and stops at 0 for a short reply
i = raw.rfind(b',"bin":', -40)
if i >= 0 and raw.endswith(b"}"):
n = int(raw[i + 7 : -1])
import numpy as np

off = (len(raw) + 1 + 3) & ~3
floats = np.frombuffer(ctypes.string_at(ptr + off, 4 * n), dtype=np.float32)
raw = raw[:i] + b"}"
finally:
lib.arcadedb_free(th, ptr)
return raw.decode("utf-8"), floats


def call_void(name: str, *args) -> None:
"""Run an entry point that returns nothing (``arcadedb_n_set_upcall``)."""
lib = _lib or _load()
Expand Down
19 changes: 12 additions & 7 deletions src/arcadedb/_native/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
from . import needs_jvm
from .codec import RID

_JAVA_OBJECT = None

def _java_object():
from .database import _JavaObject

return _JavaObject()
def _java_object():
"""A stand-in for the Java object of a record (any use of it says so); the class is looked up on first use, as database.py imports this module."""
global _JAVA_OBJECT
if _JAVA_OBJECT is None:
from .database import _JavaObject as _JAVA_OBJECT
return _JAVA_OBJECT()


_UNSAVED = "#-1:-1"
Expand Down Expand Up @@ -191,14 +195,16 @@ def get_out(self) -> Vertex:
return self._end("outV")


_CLASS_OF = {"v": Vertex, "e": Edge}


def _wrap(database, row):
"""The wrapper for a result row that is a record (vertex, edge or document); None for a projection."""
meta = row._meta
if not meta:
return None
rid, type_name, kind = meta
cls = {"v": Vertex, "e": Edge}.get(kind, Document)
return cls(database, type_name, rid, row._props)
return _CLASS_OF.get(kind, Document)(database, type_name, rid, row._props)


def _wrap_dict(database, record: dict, kind: str = "d"):
Expand All @@ -207,5 +213,4 @@ def _wrap_dict(database, record: dict, kind: str = "d"):
rid = props.pop("@rid", None)
type_name = props.pop("@type", None)
kind = props.pop("@cat", kind)
cls = {"v": Vertex, "e": Edge}.get(kind, Document)
return cls(database, type_name, rid, props)
return _CLASS_OF.get(kind, Document)(database, type_name, rid, props)
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
collect_ignore.append("test_native_heap.py")
# the native build's callback table (extras._callbacks), which the async executor releases its ids from
collect_ignore.append("test_native_async_callbacks.py")
# the short call path of a point read (codec.encode_params, lib.exec_query, first()), observed on the native library
collect_ignore.append("test_native_point_read.py")
if not os.path.isfile(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "docs", "index.md")
):
Expand Down
Loading
Loading