From 1c56720990d9d48691448b8eb9317b0c0ce0ccad Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 25 Oct 2026 17:37:00 +0900 Subject: [PATCH] Record ids in rows: tests and docs for the contract both wheels share A record id inside a row (SELECT @rid, a LINK property, a list of links) is an RID object: str() is the text, equal RIDs hash alike, RIDs sort by bucket and position, getBucketId/getPosition/toString/isValid work, and an RID goes back in as a query parameter and to lookup_by_rid. get_rid() is text. The behaviour is already the engine's own RID through JPype; this adds the test file (12 tests) and a docs section, and says that lookup_by_rid also takes the RID object a row holds. rid == "#10:5" is deliberately not part of the contract. Co-Authored-By: Claude Sonnet 5.5 --- .../pages/how-to/databases-and-queries.adoc | 14 ++ src/arcadedb/core.py | 2 +- tests/test_record_id_values.py | 224 ++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 tests/test_record_id_values.py diff --git a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc index 6e47e775..a879a6d7 100644 --- a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc +++ b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc @@ -217,6 +217,20 @@ Bind values as parameters instead of pasting them into the statement text. Ident * `first()` returns the first row, or `None` when there is none, so `if db.query(...).first():` tests whether anything matched. * `to_list()` materialises the whole result as a list of dicts. Use it when you need the whole result in memory; iterate when you stop early or the result is large. +=== Record ids in rows + +A record id inside a row value (`SELECT @rid AS r`, a `LINK` property, a list of links such as `out('Knows').@rid`) comes back as an RID object, not as text. `str(rid)` is the text (`"#10:5"`), so is an f-string, and `get_rid()` always returns text or `None`. Two RIDs of the same record are equal and hash alike, so they work in sets and as dict keys, and RIDs sort by bucket, then position. `getBucketId()`, `getPosition()`, `toString()` and `isValid()` work on the object. It goes back in as a query parameter (`WHERE @rid = ?`, or a list passed by name) and to `db.lookup_by_rid()`, as itself or as its text. + +[source,python,subs="attributes+"] +---- +row = db.query("sql", "SELECT @rid AS r, name FROM User LIMIT 1").first() +rid = row.get("r") +print(str(rid), rid.getBucketId(), rid.getPosition()) # #1:0 1 0 +print(db.lookup_by_rid(rid).get("name")) # the same record, from the object or from str(rid) +---- + +Compare record ids as `str(rid)`. A row also has an identity of its own: `result.get_rid()` returns it for a record row, an `expand()` row and a projection of `@rid` that does not alias it, and `None` for any other projection. + === Pagination Page a large type by `@rid` instead of `SKIP`: each page is `WHERE @rid > ? LIMIT ?`, and the cursor is the `@rid` of the last row of the previous page. Start with `"#-1:-1"`. The script above runs the loop with a page size of one so that two short pages print. diff --git a/src/arcadedb/core.py b/src/arcadedb/core.py index 998aa37b..995e0c12 100644 --- a/src/arcadedb/core.py +++ b/src/arcadedb/core.py @@ -1334,7 +1334,7 @@ def lookup_by_rid(self, rid: str) -> Any: Lookup a record by its RID. Args: - rid: Record ID string (e.g. "#10:5") + rid: Record ID string (e.g. "#10:5"), or the RID object a row holds Returns: Record object (Vertex, Document, or Edge) diff --git a/tests/test_record_id_values.py b/tests/test_record_id_values.py new file mode 100644 index 00000000..3888a1d9 --- /dev/null +++ b/tests/test_record_id_values.py @@ -0,0 +1,224 @@ +"""A record id inside a row: the same behaviour on the JVM wheel and the native wheel. + +The contract (docs page "Databases and queries", "Record ids in rows"): + +- a record id that sits in a row value (``SELECT @rid``, a LINK property, a list of links) is an + RID object, not text: ``str(rid)`` is the text (``"#10:5"``), two RIDs of the same record are + equal and hash alike, and RIDs sort by bucket and position; +- ``getBucketId()``, ``getPosition()``, ``toString()`` and ``isValid()`` work on it (the Java + ``RID`` has them; the native wheel's ``RID`` offers the same names); +- an RID goes back in as a query parameter, and to ``lookup_by_rid``, as itself or as its text; +- ``get_rid()`` is text (or ``None``) on every wheel. + +Before this file the JVM wheel handed out a Java ``RID`` and the native wheel its own ``RID`` +class, and nothing pinned what the two had in common. Comparing an RID with a string (``rid == +"#10:5"``) is False on the JVM wheel and True on the native wheel, so it is not part of the +contract: compare ``str(rid)``. +""" + +import re + +import pytest + +RID_TEXT = re.compile(r"^#\d+:\d+$") + + +@pytest.fixture +def graph_db(temp_db): + db = temp_db + db.command("sql", "CREATE VERTEX TYPE Person") + db.command("sql", "CREATE EDGE TYPE Knows") + db.command("sql", "CREATE PROPERTY Person.friend LINK") + with db.transaction(): + db.command("sql", "CREATE VERTEX Person SET name = 'ann'").close() + db.command("sql", "CREATE VERTEX Person SET name = 'bob'").close() + db.command("sql", "CREATE VERTEX Person SET name = 'cy'").close() + for target in ("bob", "cy"): + db.command( + "sql", + "CREATE EDGE Knows FROM (SELECT FROM Person WHERE name = 'ann') " + "TO (SELECT FROM Person WHERE name = ?)", + target, + ).close() + db.command( + "sql", + "UPDATE Person SET friend = (SELECT @rid FROM Person WHERE name = 'bob') " + "WHERE name = 'ann'", + ).close() + return db + + +def _rid_of(db, name): + return ( + db.query("sql", "SELECT @rid AS r FROM Person WHERE name = ?", name) + .first() + .get("r") + ) + + +def test_rid_in_a_row_prints_as_its_text(graph_db): + rid = _rid_of(graph_db, "ann") + assert not isinstance(rid, str) + assert RID_TEXT.match(str(rid)) + assert f"{rid}" == str(rid) + assert f"x{rid}x" == f"x{str(rid)}x" + + +def test_every_row_path_gives_the_same_kind_of_value(graph_db): + sql = "SELECT @rid AS r FROM Person WHERE name = 'ann'" + via_get = graph_db.query("sql", sql).first().get("r") + via_list = graph_db.query("sql", sql).to_list()[0]["r"] + via_dict = graph_db.query("sql", sql).first().to_dict()["r"] + via_iter = next(iter(graph_db.query("sql", sql).iter_dicts()))["r"] + assert type(via_get) is type(via_list) is type(via_dict) is type(via_iter) + assert str(via_get) == str(via_list) == str(via_dict) == str(via_iter) + + +def test_same_record_is_equal_and_hashes_alike(graph_db): + first = _rid_of(graph_db, "ann") + second = _rid_of(graph_db, "ann") + other = _rid_of(graph_db, "bob") + assert first == second + assert not (first != second) + assert first != other + assert hash(first) == hash(second) + assert len({first, second, other}) == 2 + assert {first: "x"}[second] == "x" + + +def test_rids_sort_by_bucket_and_position(graph_db): + rids = [ + r["r"] for r in graph_db.query("sql", "SELECT @rid AS r FROM Person").to_list() + ] + assert len(rids) == 3 + + def by_numbers(rid): + bucket, position = str(rid)[1:].split(":") + return int(bucket), int(position) + + assert [str(r) for r in sorted(rids)] == [ + str(r) for r in sorted(rids, key=by_numbers) + ] + lowest, highest = sorted(rids)[0], sorted(rids)[-1] + assert lowest < highest + assert highest > lowest + assert lowest <= lowest + assert highest >= highest + + +def test_bucket_and_position_accessors(graph_db): + rid = _rid_of(graph_db, "bob") + bucket, position = (int(part) for part in str(rid)[1:].split(":")) + assert int(rid.getBucketId()) == bucket + assert int(rid.getPosition()) == position + assert str(rid.toString()) == str(rid) + assert bool(rid.isValid()) is True + + +def test_link_property_is_a_rid_of_the_target(graph_db): + bob = _rid_of(graph_db, "bob") + row = graph_db.query("sql", "SELECT friend FROM Person WHERE name = 'ann'").first() + assert row.get("friend") == bob + assert type(row.get("friend")) is type(bob) + record = ( + graph_db.query("sql", "SELECT FROM Person WHERE name = 'ann'") + .first() + .get_vertex() + ) + assert record.get("friend") == bob + assert record.to_dict()["friend"] == bob + assert str(record.get("friend")) == graph_db.lookup_by_rid(str(bob)).get_rid() + + +def test_list_of_rids(graph_db): + row = graph_db.query( + "sql", "SELECT out('Knows').@rid AS ids FROM Person WHERE name = 'ann'" + ).first() + ids = row.get("ids") + assert len(ids) == 2 + assert {str(i) for i in ids} == { + str(_rid_of(graph_db, "bob")), + str(_rid_of(graph_db, "cy")), + } + assert all(type(i) is type(_rid_of(graph_db, "bob")) for i in ids) + + +def test_rid_goes_back_in_as_a_parameter(graph_db): + bob = _rid_of(graph_db, "bob") + cy = _rid_of(graph_db, "cy") + + def names(rows): + return sorted(r.get("name") for r in rows) + + assert names( + graph_db.query("sql", "SELECT name FROM Person WHERE @rid = ?", bob) + ) == ["bob"] + assert names( + graph_db.query("sql", "SELECT name FROM Person WHERE @rid = :r", {"r": bob}) + ) == ["bob"] + # (a lone list argument is the parameter list itself, so a list value goes by name) + assert names( + graph_db.query( + "sql", "SELECT name FROM Person WHERE @rid IN :ids", {"ids": [bob, cy]} + ) + ) == ["bob", "cy"] + # the text works as a parameter on both wheels as well + assert names( + graph_db.query("sql", "SELECT name FROM Person WHERE @rid = ?", str(bob)) + ) == ["bob"] + + +def test_lookup_by_rid_takes_the_rid_or_its_text(graph_db): + bob = _rid_of(graph_db, "bob") + assert graph_db.lookup_by_rid(bob).get("name") == "bob" + assert graph_db.lookup_by_rid(str(bob)).get("name") == "bob" + assert graph_db.lookup_by_rid(bob).get_rid() == str(bob) + + +def test_get_identity_is_the_same_kind_of_value(graph_db): + bob = _rid_of(graph_db, "bob") + vertex = graph_db.lookup_by_rid(str(bob)) + assert vertex.get_identity() == bob + assert type(vertex.get_identity()) is type(bob) + assert str(vertex.get_identity()) == vertex.get_rid() + + +def test_get_rid_is_text_for_every_kind_of_row(graph_db): + record = graph_db.query("sql", "SELECT FROM Person WHERE name = 'ann'").first() + assert isinstance(record.get_rid(), str) and RID_TEXT.match(record.get_rid()) + assert isinstance(record.get_vertex().get_rid(), str) + + edge = graph_db.query("sql", "SELECT FROM Knows").first() + assert isinstance(edge.get_rid(), str) and RID_TEXT.match(edge.get_rid()) + assert isinstance(edge.get_edge().get_rid(), str) + + expanded = graph_db.query( + "sql", "SELECT expand(out('Knows')) FROM Person WHERE name = 'ann'" + ).first() + assert isinstance(expanded.get_rid(), str) + + +def test_get_rid_of_a_projection(graph_db): + # a projection of @rid is a row with that identity, an aliased one or an aggregate is not + plain = graph_db.query( + "sql", "SELECT @rid, name FROM Person WHERE name = 'ann'" + ).first() + assert plain.get_rid() == str(_rid_of(graph_db, "ann")) + edge = graph_db.query("sql", "SELECT @rid FROM Knows").first() + assert isinstance(edge.get_rid(), str) and RID_TEXT.match(edge.get_rid()) + assert ( + graph_db.query("sql", "SELECT @rid AS r, name FROM Person WHERE name = 'ann'") + .first() + .get_rid() + is None + ) + assert ( + graph_db.query("sql", "SELECT name FROM Person WHERE name = 'ann'") + .first() + .get_rid() + is None + ) + assert ( + graph_db.query("sql", "SELECT count(*) AS c FROM Person").first().get_rid() + is None + )