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
36 changes: 33 additions & 3 deletions src/arcadedb/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ def _graph_calls():
return _GRAPH_CALLS or None


def _reader_calls():
"""``(ReaderCalls, results)``: the ``com.arcadedb.python.ReaderCalls`` bridge class
(None, with the bridge's usual one warning, when the jar is missing, which sends the
caller down the plain path) and the ``results`` module that holds the readers of its
text. The class is looked up on every call, as ``results._bridge_class`` caches it:
the JVM may not have started yet, and an answer given then must not stick."""
from . import results

return results._bridge_class("ReaderCalls"), results


class Document:
"""Wrapper for ArcadeDB Document.

Expand All @@ -66,9 +77,16 @@ def _check_open(self) -> None:
def _property_names_tuple(self) -> Tuple[str, ...]:
self._check_open()
if self._property_names_cache is None:
self._property_names_cache = tuple(
str(name) for name in self._java_document.getPropertyNames()
)
reader, results = _reader_calls()
if reader is not None:
# the names as one JSON text instead of one JPype read per name
self._property_names_cache = results._loads_names(
reader.documentNames(self._java_document)
)
else:
self._property_names_cache = tuple(
str(name) for name in self._java_document.getPropertyNames()
)
return self._property_names_cache

@staticmethod
Expand Down Expand Up @@ -177,6 +195,18 @@ def get_identity(self):

def to_dict(self, convert_types: bool = True) -> Dict[str, Any]:
"""Convert to dictionary."""
if convert_types:
reader, results = _reader_calls()
if reader is not None:
# One crossing for the whole record, and no JPype read of a name
# or a value: Java writes it as the typed JSON of TypedRows, the
# values keep the types convert_java_to_python gives them.
self._check_open()
row = results._loads_typed_row(reader.documentJson(self._java_document))
# the names of this read, so a property set since an earlier
# read is not missing from get_property_names()
self._property_names_cache = tuple(row)
return row
property_names = self._property_names_tuple() # checks the database
if not convert_types:
return {name: self._java_document.get(name) for name in property_names}
Expand Down
61 changes: 53 additions & 8 deletions src/arcadedb/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ def hook(obj):
return hook


def _loads_typed_row(java_row):
"""The dict of one row or record that ``ReaderCalls.rowJson`` / ``documentJson`` wrote.

``java_row`` is ``{String json, Object[] side}``. The side array is read only
when the text carries the tag character (the writer escapes it as
``\\u0001``): a row of plain values costs one array read and one string read,
and the C ``json`` decoder, and no other JPype call.
"""
text = str(java_row[0])
if "\\u0001" not in text:
return json.loads(text)
return json.loads(text, object_hook=_typed_hook(java_row[1]))


def _loads_names(java_text):
"""The property names ``ReaderCalls.rowNames`` / ``documentNames`` wrote, as a tuple."""
return tuple(json.loads(str(java_text)))


def _bridge_class(name):
"""Cached bridge-class lookup; None (with one warning) if unavailable.

Expand Down Expand Up @@ -374,7 +393,15 @@ def _drained(self) -> None:
def __next__(self) -> "Result":
if self._readable():
try:
if self._java_result_set.hasNext():
reader = _bridge_class("ReaderCalls")
if reader is not None:
# hasNext() and next() in ONE crossing: the row is still read
# when it is asked for, and the end is the same null as
# hasNext() false
java_row = reader.nextOrNull(self._java_result_set)
if java_row is not None:
return Result(java_row, self._database)
elif self._java_result_set.hasNext():
return Result(self._java_result_set.next(), self._database)
except JException as exc:
raise _read_error(exc) from exc
Expand Down Expand Up @@ -1246,9 +1273,16 @@ def _needs_database(self) -> bool:
def _property_names_tuple(self) -> Tuple[str, ...]:
self._check_open()
if self._property_names_cache is None:
self._property_names_cache = tuple(
str(name) for name in self._java_result.getPropertyNames()
)
reader = _bridge_class("ReaderCalls")
if reader is not None:
# the names as one JSON text instead of one JPype read per name
self._property_names_cache = _loads_names(
reader.rowNames(self._java_result)
)
else:
self._property_names_cache = tuple(
str(name) for name in self._java_result.getPropertyNames()
)
return self._property_names_cache

def has_property(self, name: str) -> bool:
Expand Down Expand Up @@ -1414,10 +1448,21 @@ def to_dict(self, convert_types: bool = True) -> Dict[str, Any]:
"""
self._check_open()
if convert_types:
# One crossing for the whole row (names and values) instead of one
# per property: a JPype call costs microseconds of dispatch, so a
# small row was dominated by the number of calls. The values are
# the engine's own objects, converted exactly as below.
# One crossing for the whole row, and no JPype read of a name or a
# value: Java writes the row as the typed JSON of TypedRows (the
# transport of to_list()), so a row of plain values costs the call,
# one array read and one string read. The values have the types
# convert_java_to_python gives them.
reader = _bridge_class("ReaderCalls")
if reader is not None:
row = _loads_typed_row(reader.rowJson(self._java_result))
if self._property_names_cache is None:
self._property_names_cache = tuple(row)
return row
# Without ReaderCalls: one crossing for the whole row (names and
# values) instead of one per property, then a JPype read per name and
# value. The values are the engine's own objects, converted exactly
# as below.
row_access = _bridge_class("RowAccess")
if row_access is not None:
pair = row_access.namesAndValues(self._java_result)
Expand Down
107 changes: 107 additions & 0 deletions src/java/com/arcadedb/python/ReaderCalls.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Python-bindings bridge: the readers of records and rows in one crossing.
*
* A JPype call costs a few microseconds of dispatch, and so does every read of a Java object that comes back
* (a String, an array element, a set element). A record of three properties took 18 of them to become a dict
* (getPropertyNames, one hasNext/next pair per name, one get per property, one conversion per name and value), a row
* from a result set 14, a call for the property names 12, and each row of a result set two (hasNext, next).
*
* This writes a record or a row as the typed JSON of TypedRows (see there for the tags: the values JSON cannot carry
* exactly are tagged, the engine's own objects such as RIDs, embedded documents and float[] go to a side array that
* Python reads only when the JSON refers to it), so Python makes one call, reads one String, and parses it with the C
* json module. Values keep the types convert_java_to_python gives them: the typed JSON is the transport
* ResultSet.to_list() already uses, and a test compares every value type with the per-property path.
*
* Compiled into arcadedb-python-bridge.jar during the wheel build (scripts/Dockerfile.build and
* scripts/build-native.sh) and consumed by Result.to_dict(), Document.to_dict(), the property-name readers of both,
* and ResultSet.__next__() in the Python bindings.
*/
package com.arcadedb.python;

import com.arcadedb.database.Document;
import com.arcadedb.query.sql.executor.Result;
import com.arcadedb.query.sql.executor.ResultSet;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public final class ReaderCalls {

private ReaderCalls() {
}

/**
* The next row of a result set, or null when it has no more: the {@code hasNext()} and {@code next()} of one Python
* {@code __next__} in one crossing. A row is still read when it is asked for (no batch is taken ahead), and an error
* in the statement surfaces from here as it did from either call.
*/
public static Result nextOrNull(final ResultSet rs) {
return rs.hasNext() ? rs.next() : null;
}

/**
* One row as {@code {String json, Object[] side}}: {@code json} is a row object in the typed JSON of
* {@link TypedRows}, {@code side} holds the engine objects the JSON refers to by index. The properties are in the
* order {@code getPropertyNames()} gives them.
*/
public static Object[] rowJson(final Result row) {
final StringBuilder sb = new StringBuilder(128);
final List<Object> side = new ArrayList<>();
TypedRows.appendRow(sb, row, side);
return new Object[] { sb.toString(), side.toArray() };
}

/**
* One record as {@code {String json, Object[] side}}, like {@link #rowJson(Result)}: the properties are the ones
* {@code getPropertyNames()} lists, each value is {@code get(name)}.
*/
public static Object[] documentJson(final Document doc) {
final String[] names = doc.getPropertyNames().toArray(new String[0]);
final StringBuilder sb = new StringBuilder(128);
final List<Object> side = new ArrayList<>();
for (final String name : names)
if (TypedRows.TAG.equals(name)) {
// a property named like the tag: hand the names and values over as they are, Python pairs them
final Object[] values = new Object[names.length];
for (int i = 0; i < names.length; i++)
values[i] = doc.get(names[i]);
sb.append("{\"\\u0001\":[\"r\",0]}");
side.add(new Object[] { names, values });
return new Object[] { sb.toString(), side.toArray() };
}
sb.append('{');
for (int i = 0; i < names.length; i++) {
if (i > 0)
sb.append(',');
TypedRows.quote(sb, names[i]);
sb.append(':');
TypedRows.append(sb, doc.get(names[i]), side);
}
sb.append('}');
return new Object[] { sb.toString(), side.toArray() };
}

/** The property names of a row as one JSON array of strings, in the order {@code getPropertyNames()} gives them. */
public static String rowNames(final Result row) {
return namesJson(row.getPropertyNames());
}

/** The property names of a record as one JSON array of strings, in the order {@code getPropertyNames()} gives them. */
public static String documentNames(final Document doc) {
return namesJson(doc.getPropertyNames());
}

private static String namesJson(final Set<String> names) {
final StringBuilder sb = new StringBuilder(16 * names.size() + 2);
sb.append('[');
boolean first = true;
for (final String name : names) {
if (!first)
sb.append(',');
first = false;
TypedRows.quote(sb, name);
}
return sb.append(']').toString();
}
}
4 changes: 2 additions & 2 deletions src/java/com/arcadedb/python/TypedRows.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static Object[] nextRows(final ResultSet rs, final int max) {
return new Object[] { sb.toString(), side.toArray(), drained };
}

private static void appendRow(final StringBuilder sb, final Result row, final List<Object> side) {
static void appendRow(final StringBuilder sb, final Result row, final List<Object> side) {
final String[] names = row.getPropertyNames().toArray(new String[0]);
for (final String name : names)
if (TAG.equals(name)) {
Expand Down Expand Up @@ -287,7 +287,7 @@ private static StringBuilder pad(final StringBuilder sb, final int value, final
* A JSON string. Surrogates are always written as \\u escapes (a lone one is not valid UTF-16 text to hand across the
* bridge), as are control characters.
*/
private static void quote(final StringBuilder sb, final String s) {
static void quote(final StringBuilder sb, final String s) {
sb.append('"');
final int length = s.length();
for (int i = 0; i < length; i++) {
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"test_build_no_uv_refresh.py": "build scripts of the JVM wheel",
"test_build_wheel_check.py": "build scripts of the JVM wheel",
"test_query_vector_params_jvm.py": "the java.util objects the JVM build's parameter conversion builds",
"test_reader_crossings.py": "crossing counts of the JVM build's record and row readers (JPype proxies of the Java objects and the bridge classes)",
}
if BACKEND == "native" and not os.environ.get("NATIVE_PARITY_ALL"):
collect_ignore.extend(NATIVE_NOT_APPLICABLE)
Expand Down
Loading
Loading