diff --git a/native/src/main/java/org/humemai/nativelib/NativeCodec.java b/native/src/main/java/org/humemai/nativelib/NativeCodec.java index 189dab7..59b7c94 100644 --- a/native/src/main/java/org/humemai/nativelib/NativeCodec.java +++ b/native/src/main/java/org/humemai/nativelib/NativeCodec.java @@ -36,6 +36,7 @@ import java.util.Calendar; import java.util.Collection; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -185,6 +186,18 @@ static void append(final StringBuilder sb, final Object value) { appendDocument(sb, doc); } else if (value instanceof Identifiable id) { tagged(sb, "R").append('"').append(id.getIdentity().toString()).append("\"]}"); + } else if (value instanceof Result r) { + // A query row nested in a value, for example an entry of schema:types.properties (#195): its element when it has one, as a + // Document value is written, else its properties as a map. Before, it fell through to String.valueOf and reached Python as a str. + final Optional element = r.isElement() ? r.getElement() : Optional.empty(); + if (element.isPresent()) + appendDocument(sb, element.get()); + else { + final Map properties = new LinkedHashMap<>(); + for (final String name : r.getPropertyNames()) + properties.put(name, r.getProperty(name)); + append(sb, properties); + } } else if (value instanceof Map map) { boolean stringKeys = true; for (final Object key : map.keySet()) diff --git a/src/arcadedb/_native/columnar.py b/src/arcadedb/_native/columnar.py index 4bb79f1..f288043 100644 --- a/src/arcadedb/_native/columnar.py +++ b/src/arcadedb/_native/columnar.py @@ -15,7 +15,9 @@ them. """ -import re +from .schema_objects import ( # the schema-entry reader shared with NativeType (#195) + _name_type, +) try: import numpy as _np @@ -25,7 +27,6 @@ _INT64_MAX = 2**63 - 1 _INT32_MAX = 2**31 - 1 _NoneType = type(None) -_PROPERTY = re.compile(r"name: (.+?), type: ([A-Z_]+)") class Packed: @@ -75,10 +76,10 @@ def _declared_types(db, type_name): if current in seen or current not in rows: continue seen.add(current) - for text in rows[current].get("properties") or []: - m = _PROPERTY.search(text) - if m: - out.setdefault(m.group(1), m.group(2)) + for entry in rows[current].get("properties") or []: + name, kind = _name_type(entry) # a dict since #195, the text form before + if name and kind: + out.setdefault(name, str(kind)) queue.extend(rows[current].get("parentTypes") or []) return out diff --git a/src/arcadedb/_native/schema_objects.py b/src/arcadedb/_native/schema_objects.py index 25059cb..c1ccbfe 100644 --- a/src/arcadedb/_native/schema_objects.py +++ b/src/arcadedb/_native/schema_objects.py @@ -13,6 +13,16 @@ _PROPERTY = re.compile(r"name: (.+?), type: ([A-Z_]+)") +def _name_type(entry): + """(name, type) of an entry of a ``schema:types`` row's ``properties``: a dict since #195 (the codec writes a nested row + as a map), the engine's text form of the row before it; (None, None) when neither. + """ + if isinstance(entry, dict): + return entry.get("name"), entry.get("type") + m = _PROPERTY.search(entry) if isinstance(entry, str) else None + return (m.group(1), m.group(2)) if m else (None, None) + + class NativeType: """A type as a ``schema:types`` row.""" @@ -26,11 +36,7 @@ def getType(self) -> str: return self._row.get("type") def getPropertyNames(self): - return [ - m.group(1) - for m in map(_PROPERTY.search, self._row.get("properties") or []) - if m - ] + return [n for n, _ in map(_name_type, self._row.get("properties") or []) if n] def existsProperty(self, name: str) -> bool: return name in self.getPropertyNames() diff --git a/src/arcadedb/dataframe.py b/src/arcadedb/dataframe.py index 25738b1..9d3ee0b 100644 --- a/src/arcadedb/dataframe.py +++ b/src/arcadedb/dataframe.py @@ -156,6 +156,16 @@ def _is_native(db) -> bool: _PROPERTY = re.compile(r"name: (.+?), type: ([A-Z_]+)") +def _property_name_type(entry): + """(name, type) of an entry of a native ``schema:types`` row's ``properties``: a dict since #195, the engine's text form + of the row before it; (None, None) when neither. Kept here, not imported from ``_native``, so the JVM build never loads it. + """ + if isinstance(entry, dict): + return entry.get("name"), entry.get("type") + m = _PROPERTY.search(entry) if isinstance(entry, str) else None + return (m.group(1), m.group(2)) if m else (None, None) + + def _declared_types_native( db, type_name: str, names: List[str] ) -> Tuple[bool, Dict[str, str]]: @@ -180,10 +190,10 @@ def _declared_types_native( if current in seen or current not in rows: continue seen.add(current) - for text in rows[current].get("properties") or []: - m = _PROPERTY.search(text) - if m and m.group(1) in names: - declared.setdefault(m.group(1), m.group(2)) + for entry in rows[current].get("properties") or []: + name, kind_name = _property_name_type(entry) + if name in names and kind_name: + declared.setdefault(name, str(kind_name)) queue.extend(rows[current].get("parentTypes") or []) return kind == "vertex", declared diff --git a/tests/test_schema_nested_rows.py b/tests/test_schema_nested_rows.py new file mode 100644 index 0000000..3e343cd --- /dev/null +++ b/tests/test_schema_nested_rows.py @@ -0,0 +1,40 @@ +"""#195: a query row nested in a value (the entries of `schema:types.properties`) reaches Python as a map on both builds. + +The native codec wrote a nested Result through String.valueOf, so each entry arrived as a str and code that reads the schema +(examples/04_csv_import_documents.py) stopped with AttributeError on the native package only. +""" + +import arcadedb + + +def _as_map(entry): + # the JVM build hands back the engine's Result object, the native build a dict + return dict(entry.toMap()) if hasattr(entry, "toMap") else entry + + +def test_schema_type_properties_are_maps(temp_db_path): + with arcadedb.create_database(temp_db_path) as db: + db.command("sql", "CREATE DOCUMENT TYPE Movie") + db.command("sql", "CREATE PROPERTY Movie.title STRING") + db.command("sql", "CREATE PROPERTY Movie.year INTEGER") + + rows = db.query( + "sql", "SELECT properties FROM schema:types WHERE name = 'Movie'" + ).to_list() + assert len(rows) == 1 + entries = list(rows[0].get("properties")) + assert len(entries) == 2 + + maps = [_as_map(e) for e in entries] + assert all(isinstance(m, dict) for m in maps), [ + type(e).__name__ for e in entries + ] + by_name = {str(m["name"]): m for m in maps} + assert set(by_name) == {"title", "year"} + assert str(by_name["title"]["type"]).upper() == "STRING" + assert str(by_name["year"]["type"]).upper() == "INTEGER" + + if arcadedb.__backend__ == "native": + assert all(isinstance(e, dict) for e in entries), [ + type(e).__name__ for e in entries + ]