# Graph values A query that returns a whole vertex, edge or path gets back a `Vertex`, an `Edge` or a `Path`. A query that returns properties gets back ordinary Python values. This page describes the first kind, and how either kind is read. | The server sends | You get | |---|---| | `vertex` | `agensgraph.Vertex` | | `edge` | `agensgraph.Edge` | | `graphpath` | `agensgraph.Path` | | `graphid` | `agensgraph.GraphId` | | a list of vertices or edges, such as `collect(n)` or `nodes(p)` | a `list` of them | | a property, or anything else `jsonb` | the JSON value: `str`, `int`, `float`, `bool`, `None`, `list`, `dict` | | any other SQL type | what psycopg reads it as | ## Vertices ```pycon >>> (keanu,) = conn.execute_query( ... "MATCH (p:person {name: 'Keanu Reeves'}) RETURN p" ... ).records[0] >>> keanu Vertex(person[4.1]) >>> keanu.id GraphId(labid=4, locid=1) >>> keanu.label 'person' >>> keanu.properties {'born': 1964, 'name': 'Keanu Reeves'} >>> keanu.get("born"), keanu.get("died", "-") (1964, '-') ``` | | | |---|---| | `id` | the vertex's identity, a `GraphId` | | `label` | the label it was created with | | `properties` | its property map, a `dict` | | `get(key, default=None)` | one property | | `properties_json()` | the property map as JSON `bytes` | `label` is the vertex's own label, not its ancestry. A vertex of a label that inherits from another has the child's name here; Cypher's `labels(n)` returns the whole list, own label first. The order of the keys in `properties` is the order `jsonb` stores them in, which is by key length and then by bytes, not the order they were written in. ### A property map is decoded when you first read it The property map arrives as JSON bytes and stays that way until something reads `properties` or `get()`. A query that sorts, counts or passes vertices on without looking inside them never pays for decoding their maps, and what that saves grows with the map: it matters most for a map that holds an embedding. `properties_json()` hands the bytes over as they arrived, without decoding them, if nothing has read the map yet. ## Edges ```pycon >>> (neo,) = conn.execute_query( ... "MATCH (:person {name: 'Keanu Reeves'})-[r:acted_in]->(:movie {title: 'The Matrix'})" ... " RETURN r" ... ).records[0] >>> neo Edge(acted_in[5.1] 4.1->3.1) >>> neo.start, neo.end (GraphId(labid=4, locid=1), GraphId(labid=3, locid=1)) >>> neo.properties {'role': 'Neo'} ``` An `Edge` has everything a `Vertex` has, plus `start` and `end`: the identities of the vertex it leaves and the vertex it enters. They are `GraphId`s, not vertices; return the vertices too if you need them. ## Paths ```pycon >>> (path,) = conn.execute_query( ... "MATCH p = (:movie {title: 'The Matrix'})-[:related_to*2]->" ... "(:movie {title: 'The Matrix Revolutions'}) RETURN p" ... ).records[0] >>> path Path(3 vertices, 2 edges) >>> path.length 2 >>> len(path) 5 >>> list(path) [Vertex(movie[3.1]), Edge(related_to[7.1] 3.1->3.2), Vertex(movie[3.2]), Edge(related_to[7.2] 3.2->3.3), Vertex(movie[3.3])] >>> path.start, path.end (Vertex(movie[3.1]), Vertex(movie[3.3])) ``` | | | |---|---| | `vertices`, `edges` | the path's vertices and edges, in order, as tuples | | `start`, `end` | the first and last vertex, or `None` for an empty path | | `length` | the number of hops, that is, of edges | | `len(path)` | the number of elements: vertices and edges together | | `path[i]` | an element: even positions are vertices, odd ones edges; negative indexes and slices work | | iterating | vertex, edge, vertex, ... | A path of one vertex has `length` 0 and `len` 1, and is true in a boolean context. An empty path is legal, has neither, and is false. ## Lists of elements `collect(n)`, `nodes(p)` and `relationships(p)` return lists, and arrive as Python lists of `Vertex` or `Edge`: ```pycon >>> conn.execute_query( ... "MATCH (:person {name: 'Keanu Reeves'})-[:acted_in]->(m) RETURN collect(m)" ... ).records[0][0][:2] [Vertex(movie[3.1]), Vertex(movie[3.2])] ``` A `NULL` inside such a list, which the server produces for an `OPTIONAL MATCH` that matched nothing, is `None`. ## Identities A `GraphId` is a pair of numbers: the label's id and the element's number within that label. It prints the way the server prints it: ```pycon >>> keanu.id.labid, keanu.id.locid, str(keanu.id) (4, 1, '4.1') ``` Build one from its two numbers, `GraphId(4, 1)`. `packed` is the single 64-bit integer the server stores, label id in the top 16 bits, and `GraphId.from_packed()` reverses it; identities sort in the same order either way. To pass an identity you hold as text, write `%s::graphid` in the statement. ## Equality, hashing, immutability A vertex or an edge compares and hashes on its **identity alone**, as the server compares elements. The same element read twice, even with different properties in between, is equal to itself, and works as a dictionary key or in a set: ```pycon >>> again = conn.execute_query( ... "MATCH (p:person) WHERE p.born = 1964 AND p.name STARTS WITH 'K' RETURN p" ... ).records[0][0] >>> again == keanu, len({again, keanu}) (True, 1) ``` A vertex never equals an edge, even one with the same identity, and a `Path` compares on its vertices and edges. The values are read-only: none of their fields can be assigned. Read a value, then write the change with a statement. Vertices, edges and identities are also not tracked by Python's cycle collector, which keeps a large result cheap to hold. ## Two renderings The server can send a graph value in two forms. The default is its **text** rendering, the same one `psql` prints, which carries everything the driver needs. The other is its **composite** rendering, asked for per statement with `binary_=True`: ```pycon >>> text = conn.execute_query("MATCH ()-[r:acted_in]->() RETURN r ORDER BY id(r) LIMIT 3").records >>> composite = conn.execute_query( ... "MATCH ()-[r:acted_in]->() RETURN r ORDER BY id(r) LIMIT 3", binary_=True ... ).records >>> text == composite True ``` Both produce the same values. The composite form trades bandwidth for parsing: each value carries its field lengths, so the driver reads them instead of searching for where a value ends, but it also carries type information the text form leaves out. What that is worth depends on the shape of the result: edges, which are mostly fixed-width fields, gain the most, while on whole vertices and on paths, which are mostly property maps decoded the same way either way, the difference is small. It is the clear choice for [embedding vectors](../data/vectors.md), whose text is long decimal and whose composite form is the numbers themselves. Otherwise leave it off, and measure before turning it on for one slow query. The composite form names a label by its id only, so the driver looks the name up in the connection's label table, which [`conn.graph()`](connecting.md#selecting-a-graph) fills. `execute_query(binary_=True)` checks that the table is there and raises `agensgraph.errors.StaleLabelCache` if it is not. psycopg's own `binary=True`, on `execute()` or a cursor, skips that check, so use `binary_=True` on `execute_query()` or `stream()` instead. ## Numbers `jsonb` stores a number exactly, at any precision; a Python `float` does not. By default: - an **integer** of any size reads as an exact `int`; - a **non-integer** reads as a `float`, which keeps about seventeen significant digits and turns a value too small to represent, such as `1e-400`, into `0.0`. For data where those digits matter, read every non-integer as a `Decimal` instead: ```python import agensgraph agensgraph.read_numbers_exactly() # once, when the application starts ``` ```pycon >>> conn.execute_query("RETURN 3.14159265358979323846264338327950288").records [(Decimal('3.14159265358979323846264338327950288'),)] >>> agensgraph.read_numbers_exactly(False) ``` It applies to property maps and to `jsonb` columns alike, so `RETURN n` and `RETURN n.price` agree about the same stored value. It costs nothing on integers; a map of non-integers takes about four times as long to decode, and a large embedding about six. `reading_numbers_exactly()` says whether it is on. The setting is for the whole process. Set it once, before reading anything: a property map is decoded when it is first read, not when it arrives, so changing the setting while a result is in hand can give two rows of it different types. ## Handing values to something that wants JSON A graph value is not something `json.dumps` knows. Three functions convert it, in the same shape: ```pycon >>> agensgraph.to_builtins(keanu) {'id': '4.1', 'label': 'person', 'properties': {'born': 1964, 'name': 'Keanu Reeves'}} >>> agensgraph.to_builtins(neo) {'id': '5.1', 'label': 'acted_in', 'start': '4.1', 'end': '3.1', 'properties': {'role': 'Neo'}} ``` | | | |---|---| | `to_json(value)` | JSON `bytes` for a value, a row or a whole result | | `to_builtins(value)` | one graph value as `dict`s and `list`s | | `json_default` | a `default=` hook for `json.dumps` | ```pycon >>> rows = conn.execute_query( ... "MATCH (p:person {name: 'Keanu Reeves'})-[r:acted_in]->(:movie {title: 'The Matrix'})" ... " RETURN p, r" ... ).records >>> agensgraph.to_json(rows) b'[[{"id":"4.1","label":"person","properties":{"born": 1964, "name": "Keanu Reeves"}},{"id":"5.1","label":"acted_in","start":"4.1","end":"3.1","properties":{"role": "Neo"}}]]' >>> import json >>> json.dumps(rows[0][1], default=agensgraph.json_default) '{"id": "5.1", "label": "acted_in", "start": "4.1", "end": "3.1", "properties": {"role": "Neo"}}' ``` A path becomes `{"vertices": [...], "edges": [...]}`. An identity becomes the text the server prints, `"4.1"`, which goes straight back into a statement as `%s::graphid`. **`to_json` is the fast one.** A property map nobody has read is copied into the output as the bytes it arrived in, never decoded, so on results with large maps, such as embeddings, it is far faster than going through `json.dumps`, which decodes every number and writes it out again. `to_builtins` takes one graph value, not a list. Do not hand a graph value to `msgspec.json.encode` directly: it would write the value's private fields, and msgspec gives no hook to prevent it.