Inspecting a graph#

A graph’s shape lives in the server’s catalogs: which graphs exist, which labels each one has, which properties have columns of their own, and which indexes and constraints are defined. psql has no describe commands for most of these, so the driver reads them for you and returns each row as a named tuple.

Every method here exists on AsyncConnection too, with the same arguments, awaited.

Which graph is read#

Each method that reads one graph takes an optional keyword argument, graph=. Without it, the method reads the graph the session has selected with conn.graph() or SET graph_path. With it, it reads the graph you name, and sends no statement to find out which one is selected.

If neither gives a graph, the method raises ValueError:

ValueError: no graph is selected on this connection, so name the one to read about

Graphs#

>>> conn.graphs()
[Graph(name='moviekb', schema='moviekb', labels=7)]

graphs() lists every graph in the database, ordered by name. A graph is a schema, and schema is its name. labels counts the graph’s labels including the two built-in ones, ag_vertex and ag_edge, which every graph has.

Labels#

>>> for label in conn.labels():
...     print(label)
Label(id=1, name='ag_vertex', kind='v', parent=None)
Label(id=2, name='ag_edge', kind='e', parent=None)
Label(id=3, name='movie', kind='v', parent='ag_vertex')
Label(id=4, name='person', kind='v', parent='ag_vertex')
Label(id=5, name='acted_in', kind='e', parent='ag_edge')
Label(id=6, name='directed', kind='e', parent='ag_edge')
Label(id=7, name='related_to', kind='e', parent='ag_edge')

Each row is a LabelInfo. It prints as Label because that is its name inside agensgraph.introspect, but it is exported as agensgraph.LabelInfo, since agensgraph.Label is an unrelated class.

Field

id

the label’s id. It is the first number of every element’s identity: a movie is 3.n

name

the name as the server stores it

kind

'v' for a vertex label, 'e' for an edge label

parent

the label it inherits from; None only for the two built-ins

is_vertex, is_edge and is_builtin answer the obvious questions. A name is stored the way the server folded it: CREATE VLABEL Movie stores movie, because an unquoted name is folded to lower case. labels() is where to check what a label is really called.

How many elements each label holds#

>>> conn.element_counts()
{'movie': 18, 'person': 28, 'acted_in': 25, 'directed': 21, 'related_to': 11}

The count is per label, exactly: an element of a child label counts under the child, not its parent. A label with no elements is left out rather than reported as 0. The count reads no property, because the label is part of every element’s identity.

Indexes#

>>> conn.indexes()
[]
>>> conn.execute("CREATE PROPERTY INDEX ON movie (title)")
<agensgraph.connection.Cursor ...>
>>> conn.indexes()
[Index(label='movie', name='movie_title_idx', unique=False, definition='CREATE PROPERTY INDEX movie_title_idx ON movie USING btree (title)')]

indexes() lists the property indexes: indexes over a property in the map, and indexes over a property with a column of its own. definition is the statement the server would use to create the index again. Pass a label to narrow the list, conn.indexes("movie").

Two kinds of index are not listed. An index written as plain SQL over a real column of the label’s table, such as an edge’s start and end, is not a property index. And a uniqueness constraint is not listed here even though the server enforces it with an index, because it is a constraint: look in constraints().

Embedding vectors has vector_indexes(), which reads a vector index back in parts.

Constraints#

>>> conn.execute("CREATE CONSTRAINT movie_title_unique ON movie ASSERT title IS UNIQUE")
<agensgraph.connection.Cursor ...>
>>> conn.execute("CREATE CONSTRAINT movie_year_plausible ON movie ASSERT year > 1880")
<agensgraph.connection.Cursor ...>
>>> for constraint in conn.constraints("movie"):
...     print(constraint)
Constraint(label='movie', name='movie_title_unique', unique=True, definition='ASSERT (title) IS UNIQUE')
Constraint(label='movie', name='movie_year_plausible', unique=False, definition='ASSERT ((year) > cypher_to_jsonb(1880))')

constraints() lists uniqueness assertions (unique=True) and check constraints. The not-null and primary-key constraints every label table carries are left out. definition is the server’s own rendering, which is normalised: year > 1880 comes back as ASSERT ((year) > cypher_to_jsonb(1880)).

Properties with a column of their own#

From AgensGraph 2.18 a label can keep a property in a column of its own instead of in the property map, which makes reading that one property cheap and lets the column’s type be enforced. declared_properties() lists those:

>>> conn.execute("CREATE VLABEL review (score numeric GENERATED)")
<agensgraph.connection.Cursor ...>
>>> conn.declared_properties("review")
[DeclaredProperty(label='review', name='score', type='numeric', nullable=True, inherited=False)]

type is the server’s spelling of the column type, so int comes back as integer and float8 as double precision. inherited is true for a column the label has from its parent. A property kept in the map is not declared anywhere and is never listed. On a server that cannot keep a property in a column, declared_properties() returns [].

Whether the server can do it at all is asked once per connection and kept:

>>> conn.can_promote_properties()
True

Describing a graph#

describe() puts all of that together: every label, what properties each one holds, which labels its edges connect, and how many elements there are. It is meant for anything that has to explain a graph to someone else, such as a schema browser or a prompt for a language model.

>>> conn.rollback()
>>> description = conn.describe()
>>> description.counts
{'movie': 18, 'person': 28, 'acted_in': 25, 'directed': 21, 'related_to': 11}
>>> for shape in description.properties["movie"]:
...     print(shape)
PropertyShape(name='embedding', kind='array', declared=False)
PropertyShape(name='genre', kind='string', declared=False)
PropertyShape(name='plot', kind='string', declared=False)
PropertyShape(name='title', kind='string', declared=False)
PropertyShape(name='year', kind='integer', declared=False)

Field

graph

the graph described

labels

every label, as labels() returns them

properties

for each label other than the built-ins, its properties as PropertyShape(name, kind, declared), sorted by name

triples

which vertex labels each edge label connects, as Triple(start, edge, end, edge_count)

counts

element_counts()

meta_gathered

whether triples is current

A property’s kind is its JSON type, with numbers split into integer and float. For a property with a column of its own, declared is true and kind is the column’s type, read from the catalog. Everything else comes from a sample of each label, 100 elements by default. A property that none of those elements has is not reported; pass sample= to look at more.

Nothing is installed, and no property map is read in full. Labels and triples come from catalogs, the property names from the sample, and the counts from the elements’ identities alone, which reads each label’s rows but none of their properties. The database is unchanged afterwards.

Edges and the labels they connect#

triples comes from a catalog the server fills when it is asked to gather it, and the setting that does it automatically, auto_gather_graphmeta, is off by default. Until something gathers, triples is empty and meta_gathered is false:

>>> description.triples, description.meta_gathered
((), False)

refresh=True gathers first, if the catalog is not current:

>>> description = conn.describe(refresh=True)
>>> for triple in description.triples:
...     print(triple)
Triple(start='person', edge='acted_in', end='movie', edge_count=25)
Triple(start='person', edge='directed', end='movie', edge_count=21)
Triple(start='movie', edge='related_to', end='movie', edge_count=11)
>>> description.meta_gathered
True

Gathering is a write, so it cannot run in a read-only transaction. On AgensGraph 2.18 the catalog stays current until the next committed write to the graph, and meta_gathered turns false again after it; meta_is_current() asks the same question on its own. On 2.17 the server does not record whether the catalog is current, so meta_gathered is true when it holds any rows, or when the graph has no edges to describe.

The label table#

conn.graph(name) also reads the selected graph’s label ids and names into the connection’s label table, conn.label_table. The driver needs it for one thing: the server’s composite rendering of a vertex or an edge carries the label’s id and not its name, so decoding a result asked for with binary_=True looks the name up there.

The table follows the session. Anything that moves the session to another graph, including a rollback that undoes a SET graph_path, drops it, and the next catalog read finds the graph again. What does not refresh it is a new label: after creating one by DDL or by a write, call conn.refresh_labels() before reading its elements in the composite rendering. A binary read that meets a label id it does not know raises agensgraph.errors.StaleLabelCache rather than naming the wrong label. ensure_labels() refreshes the table for you.

From AgensGraph 2.18.6 the server reports graph_path to the client whenever it changes, and the driver reads it from there at no cost. On earlier servers it watches the statements that pass instead, and asks the server when it cannot tell.