mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 14:52:26 +00:00
feat: property, virtual and materialized virtual property graphs in the client
Each kind has its own create, open, describe, list and drop. Python create and open return handles: PropertyGraph.insert and rollback, and MaterializedVirtualPropertyGraph.refresh.
This commit is contained in:
+23
-12
@@ -204,18 +204,19 @@ listing a storage directory.
|
||||
|
||||
## Property Graphs
|
||||
|
||||
A property graph is read as nodes and edges, in one of two modes. An
|
||||
independent graph holds its own rows: define it with node and edge types, and
|
||||
write it with
|
||||
[insert_into_property_graph][lancedb.DBConnection.insert_into_property_graph].
|
||||
A materialized view graph reads tables: define it with node and edge tables,
|
||||
and bring it up to them with
|
||||
[refresh_property_graph][lancedb.DBConnection.refresh_property_graph]. Manage
|
||||
either with [create_property_graph][lancedb.DBConnection.create_property_graph],
|
||||
[describe_property_graph][lancedb.DBConnection.describe_property_graph],
|
||||
[rollback_property_graph][lancedb.DBConnection.rollback_property_graph],
|
||||
[list_property_graphs][lancedb.DBConnection.list_property_graphs] and
|
||||
[drop_property_graph][lancedb.DBConnection.drop_property_graph].
|
||||
A property graph is read as nodes and edges with SQL/PGQ `GRAPH_TABLE`, and
|
||||
comes in three kinds that share one namespace of names:
|
||||
|
||||
- A property graph holds its own rows. Define it with node and edge types, and
|
||||
write it with [PropertyGraph.insert][lancedb.graph.PropertyGraph.insert].
|
||||
- A virtual property graph reads tables as they are. Define it with node and
|
||||
edge tables.
|
||||
- A materialized virtual property graph has a virtual graph's definition, and
|
||||
is read as its last refresh built it: bring it up to its tables with
|
||||
[MaterializedVirtualPropertyGraph.refresh][lancedb.graph.MaterializedVirtualPropertyGraph.refresh].
|
||||
|
||||
Each kind has its own create, open, describe, list and drop, such as
|
||||
[create_virtual_property_graph][lancedb.DBConnection.create_virtual_property_graph].
|
||||
|
||||
::: lancedb.graph.NodeType
|
||||
|
||||
@@ -227,8 +228,18 @@ either with [create_property_graph][lancedb.DBConnection.create_property_graph],
|
||||
|
||||
::: lancedb.graph.Endpoint
|
||||
|
||||
::: lancedb.graph.PropertyGraph
|
||||
|
||||
::: lancedb.graph.VirtualPropertyGraph
|
||||
|
||||
::: lancedb.graph.MaterializedVirtualPropertyGraph
|
||||
|
||||
::: lancedb.graph.PropertyGraphDescription
|
||||
|
||||
::: lancedb.graph.VirtualPropertyGraphDescription
|
||||
|
||||
::: lancedb.graph.MaterializedVirtualPropertyGraphDescription
|
||||
|
||||
## Expressions
|
||||
|
||||
Type-safe expression builder for filters and projections. Use these instead
|
||||
|
||||
@@ -48,7 +48,14 @@ from .materialized_view import (
|
||||
MaterializedViewDefinition,
|
||||
)
|
||||
from .view import ViewDescription as ViewDescription
|
||||
from .graph import PropertyGraphDescription as PropertyGraphDescription
|
||||
from .graph import (
|
||||
MaterializedVirtualPropertyGraph,
|
||||
MaterializedVirtualPropertyGraphDescription,
|
||||
PropertyGraph,
|
||||
PropertyGraphDescription,
|
||||
VirtualPropertyGraph,
|
||||
VirtualPropertyGraphDescription,
|
||||
)
|
||||
from .table import AsyncTable, Table
|
||||
from .types import BaseTokenizerType
|
||||
from ._lancedb import Session
|
||||
@@ -585,7 +592,12 @@ __all__ = [
|
||||
"MaterializedView",
|
||||
"MaterializedViewDefinition",
|
||||
"ViewDescription",
|
||||
"PropertyGraph",
|
||||
"PropertyGraphDescription",
|
||||
"VirtualPropertyGraph",
|
||||
"VirtualPropertyGraphDescription",
|
||||
"MaterializedVirtualPropertyGraph",
|
||||
"MaterializedVirtualPropertyGraphDescription",
|
||||
"connect",
|
||||
"connect_async",
|
||||
"tokenize",
|
||||
|
||||
@@ -188,20 +188,21 @@ class Connection(object):
|
||||
async def list_views(
|
||||
self, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]: ...
|
||||
async def create_property_graph(
|
||||
async def create_graph(
|
||||
self,
|
||||
kind: str,
|
||||
name: str,
|
||||
definition_json: str,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> str: ...
|
||||
async def describe_property_graph(
|
||||
self, name: str, namespace_path: Optional[List[str]] = None
|
||||
async def describe_graph(
|
||||
self, kind: str, name: str, namespace_path: Optional[List[str]] = None
|
||||
) -> str: ...
|
||||
async def drop_property_graph(
|
||||
self, name: str, namespace_path: Optional[List[str]] = None
|
||||
async def drop_graph(
|
||||
self, kind: str, name: str, namespace_path: Optional[List[str]] = None
|
||||
) -> None: ...
|
||||
async def list_property_graphs(
|
||||
self, namespace_path: Optional[List[str]] = None
|
||||
async def list_graphs(
|
||||
self, kind: str, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]: ...
|
||||
async def insert_into_property_graph(
|
||||
self,
|
||||
@@ -210,10 +211,10 @@ class Connection(object):
|
||||
data: Any,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> str: ...
|
||||
async def refresh_property_graph(
|
||||
async def rollback_property_graph(
|
||||
self, name: str, namespace_path: Optional[List[str]] = None
|
||||
) -> str: ...
|
||||
async def rollback_property_graph(
|
||||
async def refresh_materialized_virtual_property_graph(
|
||||
self, name: str, namespace_path: Optional[List[str]] = None
|
||||
) -> str: ...
|
||||
async def list_jobs(self) -> List[JobInfo]: ...
|
||||
|
||||
+339
-206
@@ -50,15 +50,25 @@ from . import __version__
|
||||
from ._lancedb import connect as lancedb_connect # type: ignore
|
||||
from .functions import FunctionVersion, UdfDefinition
|
||||
from .graph import (
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH,
|
||||
PROPERTY_GRAPH,
|
||||
VIRTUAL_PROPERTY_GRAPH,
|
||||
AsyncMaterializedVirtualPropertyGraph,
|
||||
AsyncPropertyGraph,
|
||||
AsyncVirtualPropertyGraph,
|
||||
EdgeTable,
|
||||
EdgeType,
|
||||
MaterializedVirtualPropertyGraph,
|
||||
MaterializedVirtualPropertyGraphDescription,
|
||||
NodeTable,
|
||||
NodeType,
|
||||
PropertyGraph,
|
||||
PropertyGraphDescription,
|
||||
_definition_json,
|
||||
_description_from_json,
|
||||
VirtualPropertyGraph,
|
||||
VirtualPropertyGraphDescription,
|
||||
_tables_json,
|
||||
_types_json,
|
||||
)
|
||||
from .scannable import to_scannable
|
||||
from .job import AsyncJob, Job, _typed_job
|
||||
from .sql import AsyncQuery as AsyncSqlQuery
|
||||
from .sql import Query as SqlQuery
|
||||
@@ -1031,81 +1041,25 @@ class DBConnection(EnforceOverrides):
|
||||
def create_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[Union[NodeTable, NodeType]],
|
||||
edges: List[Union[EdgeTable, EdgeType]],
|
||||
nodes: List[NodeType],
|
||||
edges: List[EdgeType],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
"""Create a property graph.
|
||||
) -> PropertyGraph:
|
||||
"""Create a property graph: a graph that holds its own rows.
|
||||
|
||||
With [NodeTable][lancedb.graph.NodeTable] and
|
||||
[EdgeTable][lancedb.graph.EdgeTable], the graph is a materialized view
|
||||
of tables in its namespace: each node table names its key column and
|
||||
label, each edge table its label and the node keys its source and
|
||||
destination columns reference, and the graph is built from them now.
|
||||
With [NodeType][lancedb.graph.NodeType] and
|
||||
[EdgeType][lancedb.graph.EdgeType], the graph is independent: it holds
|
||||
its own rows, starts empty, and takes rows from
|
||||
[insert_into_property_graph][lancedb.db.DBConnection.insert_into_property_graph].
|
||||
A name already taken is an error. Local connections raise
|
||||
Each [NodeType][lancedb.graph.NodeType] and
|
||||
[EdgeType][lancedb.graph.EdgeType] gives one label's schema. The graph
|
||||
starts empty and takes rows from
|
||||
[PropertyGraph.insert][lancedb.graph.PropertyGraph.insert]. A name held
|
||||
by a graph of any kind is taken. Local connections raise
|
||||
``NotImplementedError``.
|
||||
|
||||
>>> import lancedb
|
||||
>>> from lancedb.graph import EdgeTable, Endpoint, NodeTable
|
||||
>>> db = lancedb.connect("db://my_database") # doctest: +SKIP
|
||||
>>> graph = db.create_property_graph(
|
||||
... "social",
|
||||
... nodes=[NodeTable("person", key="person_id", label="Person")],
|
||||
... edges=[
|
||||
... EdgeTable(
|
||||
... "knows",
|
||||
... label="KNOWS",
|
||||
... source=Endpoint("src_id", references=("person", "person_id")),
|
||||
... destination=Endpoint(
|
||||
... "dst_id", references=("person", "person_id")
|
||||
... ),
|
||||
... )
|
||||
... ],
|
||||
... ) # doctest: +SKIP
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def describe_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""What this database records about a property graph: its mode and
|
||||
definition, its current and previous commits, its size, and, for a
|
||||
materialized view, the table versions it was refreshed from.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def insert_into_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
label: str,
|
||||
data: DATA,
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
"""Insert rows into an independent property graph, as one commit.
|
||||
|
||||
The rows are one node label's or one edge label's. Nodes upsert by key:
|
||||
a key the graph holds replaces that node's properties. Edges append,
|
||||
carrying their endpoints' keys in the columns the edge type names; an
|
||||
edge whose endpoint is not a node of the graph refuses the whole
|
||||
insert. A materialized view graph is written through its tables and
|
||||
refreshed instead. Local connections raise ``NotImplementedError``.
|
||||
|
||||
>>> import pyarrow as pa
|
||||
>>> from lancedb.graph import EdgeType, NodeType
|
||||
>>> db = lancedb.connect("db://my_database") # doctest: +SKIP
|
||||
>>> db.create_property_graph(
|
||||
>>> people = db.create_property_graph(
|
||||
... "people",
|
||||
... nodes=[
|
||||
... NodeType(
|
||||
@@ -1127,35 +1081,43 @@ class DBConnection(EnforceOverrides):
|
||||
... )
|
||||
... ],
|
||||
... ) # doctest: +SKIP
|
||||
>>> db.insert_into_property_graph(
|
||||
... "people", "Person", [{"person_id": 1, "name": "Alice"}]
|
||||
>>> people.insert(
|
||||
... "Person", [{"person_id": 1, "name": "Alice"}]
|
||||
... ) # doctest: +SKIP
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def refresh_property_graph(
|
||||
def open_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""Bring a materialized view property graph up to its tables' latest
|
||||
versions.
|
||||
) -> PropertyGraph:
|
||||
"""Open the property graph named ``name``; an error if there is none.
|
||||
|
||||
Until it is refreshed, a query over a table that has changed reads the
|
||||
tables rather than the graph. Local connections raise
|
||||
``NotImplementedError``.
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def rollback_property_graph(
|
||||
def describe_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""Return a property graph to its previous commit.
|
||||
"""What this database records about a property graph: its node and edge
|
||||
types, its current and previous commits, and its size.
|
||||
|
||||
The commit rolled back from is discarded, so a second rollback in a row
|
||||
is an error. Local connections raise ``NotImplementedError``.
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def list_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the property graphs in one namespace.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
@@ -1166,17 +1128,169 @@ class DBConnection(EnforceOverrides):
|
||||
) -> None:
|
||||
"""Drop a property graph and wait for its data to be deleted.
|
||||
|
||||
The tables it reads are untouched. Local connections raise
|
||||
``NotImplementedError``.
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def list_property_graphs(
|
||||
def create_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> VirtualPropertyGraph:
|
||||
"""Create a virtual property graph over tables in its namespace.
|
||||
|
||||
Each [NodeTable][lancedb.graph.NodeTable] names the table holding one
|
||||
label's nodes and its key column, and each
|
||||
[EdgeTable][lancedb.graph.EdgeTable] the table holding one label's edges
|
||||
and the node keys its source and destination columns reference. The
|
||||
graph stores nothing else: a query reads the tables as they are then. A
|
||||
name held by a graph of any kind is taken. Local connections raise
|
||||
``NotImplementedError``.
|
||||
|
||||
>>> import lancedb
|
||||
>>> from lancedb.graph import EdgeTable, Endpoint, NodeTable
|
||||
>>> db = lancedb.connect("db://my_database") # doctest: +SKIP
|
||||
>>> graph = db.create_virtual_property_graph(
|
||||
... "social",
|
||||
... nodes=[NodeTable("person", key="person_id", label="Person")],
|
||||
... edges=[
|
||||
... EdgeTable(
|
||||
... "knows",
|
||||
... label="KNOWS",
|
||||
... source=Endpoint("src_id", references=("person", "person_id")),
|
||||
... destination=Endpoint(
|
||||
... "dst_id", references=("person", "person_id")
|
||||
... ),
|
||||
... )
|
||||
... ],
|
||||
... ) # doctest: +SKIP
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def open_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> VirtualPropertyGraph:
|
||||
"""Open the virtual property graph named ``name``; an error if there is
|
||||
none.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def describe_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> VirtualPropertyGraphDescription:
|
||||
"""What this database records about a virtual property graph: its node
|
||||
and edge tables, with the properties create resolved.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def list_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the property graphs in one namespace.
|
||||
"""The names of the virtual property graphs in one namespace.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def drop_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a virtual property graph and wait for its definition to be
|
||||
deleted. The tables it reads are untouched.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def create_materialized_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> MaterializedVirtualPropertyGraph:
|
||||
"""Create a materialized virtual property graph over tables in its
|
||||
namespace.
|
||||
|
||||
The definition is a virtual property graph's (see
|
||||
[create_virtual_property_graph][lancedb.db.DBConnection.create_virtual_property_graph]).
|
||||
The graph is built from the tables now, and read as that build until
|
||||
[refreshed][lancedb.graph.MaterializedVirtualPropertyGraph.refresh]. A
|
||||
name held by a graph of any kind is taken. Local connections raise
|
||||
``NotImplementedError``.
|
||||
|
||||
>>> db = lancedb.connect("db://my_database") # doctest: +SKIP
|
||||
>>> mvpg = db.create_materialized_virtual_property_graph(
|
||||
... "social_mv", nodes, edges
|
||||
... ) # doctest: +SKIP
|
||||
>>> mvpg.refresh() # doctest: +SKIP
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def open_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> MaterializedVirtualPropertyGraph:
|
||||
"""Open the materialized virtual property graph named ``name``; an error
|
||||
if there is none.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def describe_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""What this database records about a materialized virtual property
|
||||
graph: its node and edge tables, and what its last refresh built from
|
||||
them.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def list_materialized_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the materialized virtual property graphs in one
|
||||
namespace.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Property graph operations are not supported for this connection type"
|
||||
)
|
||||
|
||||
def drop_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a materialized virtual property graph and wait for its data to
|
||||
be deleted. The tables it reads are untouched.
|
||||
|
||||
Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
@@ -2049,72 +2163,6 @@ class LanceDBConnection(DBConnection):
|
||||
def list_views(self, *, namespace_path: Optional[List[str]] = None) -> List[str]:
|
||||
return LOOP.run(self._conn.list_views(namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def create_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[Union[NodeTable, NodeType]],
|
||||
edges: List[Union[EdgeTable, EdgeType]],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.create_property_graph(
|
||||
name, nodes, edges, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def describe_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.describe_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def insert_into_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
label: str,
|
||||
data: DATA,
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.insert_into_property_graph(
|
||||
name, label, data, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def refresh_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.refresh_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def rollback_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.rollback_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def drop_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
LOOP.run(self._conn.drop_property_graph(name, namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def list_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
return LOOP.run(self._conn.list_property_graphs(namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def list_jobs(self) -> List[JobInfo]:
|
||||
"""List server-side jobs across the database's tables."""
|
||||
@@ -3145,77 +3193,162 @@ class AsyncConnection(object):
|
||||
async def create_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[Union[NodeTable, NodeType]],
|
||||
edges: List[Union[EdgeTable, EdgeType]],
|
||||
nodes: List[NodeType],
|
||||
edges: List[EdgeType],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
"""Create a property graph: a materialized view of tables in its
|
||||
namespace, or an independent graph that holds its own rows.
|
||||
) -> AsyncPropertyGraph:
|
||||
"""Create a property graph: a graph that holds its own rows.
|
||||
|
||||
See
|
||||
[DBConnection.create_property_graph][lancedb.DBConnection.create_property_graph].
|
||||
"""
|
||||
return _description_from_json(
|
||||
await self._inner.create_property_graph(
|
||||
name, _definition_json(nodes, edges), list(namespace_path or [])
|
||||
)
|
||||
graph = AsyncPropertyGraph(self._inner, name, namespace_path)
|
||||
await self._inner.create_graph(
|
||||
PROPERTY_GRAPH, name, _types_json(nodes, edges), graph.namespace_path
|
||||
)
|
||||
return graph
|
||||
|
||||
async def open_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> AsyncPropertyGraph:
|
||||
"""Open the property graph named ``name``; an error if there is none."""
|
||||
graph = AsyncPropertyGraph(self._inner, name, namespace_path)
|
||||
await graph.describe()
|
||||
return graph
|
||||
|
||||
async def describe_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""What this database records about a property graph."""
|
||||
return _description_from_json(
|
||||
await self._inner.describe_property_graph(name, list(namespace_path or []))
|
||||
)
|
||||
|
||||
async def insert_into_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
label: str,
|
||||
data: DATA,
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
"""Insert rows of one label into an independent property graph.
|
||||
|
||||
See
|
||||
[DBConnection.insert_into_property_graph][lancedb.DBConnection.insert_into_property_graph].
|
||||
"""
|
||||
return _description_from_json(
|
||||
await self._inner.insert_into_property_graph(
|
||||
name, label, to_scannable(data), list(namespace_path or [])
|
||||
)
|
||||
)
|
||||
|
||||
async def refresh_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""Bring a materialized view property graph up to its tables."""
|
||||
return _description_from_json(
|
||||
await self._inner.refresh_property_graph(name, list(namespace_path or []))
|
||||
)
|
||||
|
||||
async def rollback_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
"""Return a property graph to its previous commit."""
|
||||
return _description_from_json(
|
||||
await self._inner.rollback_property_graph(name, list(namespace_path or []))
|
||||
)
|
||||
|
||||
async def drop_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a property graph and wait for its data to be deleted."""
|
||||
await self._inner.drop_property_graph(name, list(namespace_path or []))
|
||||
return await AsyncPropertyGraph(self._inner, name, namespace_path).describe()
|
||||
|
||||
async def list_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the property graphs in one namespace."""
|
||||
return await self._inner.list_property_graphs(list(namespace_path or []))
|
||||
return await self._inner.list_graphs(PROPERTY_GRAPH, list(namespace_path or []))
|
||||
|
||||
async def drop_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a property graph and wait for its data to be deleted."""
|
||||
await self._inner.drop_graph(PROPERTY_GRAPH, name, list(namespace_path or []))
|
||||
|
||||
async def create_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> AsyncVirtualPropertyGraph:
|
||||
"""Create a virtual property graph over tables in its namespace.
|
||||
|
||||
See
|
||||
[DBConnection.create_virtual_property_graph][lancedb.DBConnection.create_virtual_property_graph].
|
||||
"""
|
||||
graph = AsyncVirtualPropertyGraph(self._inner, name, namespace_path)
|
||||
await self._inner.create_graph(
|
||||
VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
_tables_json(nodes, edges),
|
||||
graph.namespace_path,
|
||||
)
|
||||
return graph
|
||||
|
||||
async def open_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> AsyncVirtualPropertyGraph:
|
||||
"""Open the virtual property graph named ``name``; an error if there is
|
||||
none."""
|
||||
graph = AsyncVirtualPropertyGraph(self._inner, name, namespace_path)
|
||||
await graph.describe()
|
||||
return graph
|
||||
|
||||
async def describe_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> VirtualPropertyGraphDescription:
|
||||
"""What this database records about a virtual property graph."""
|
||||
return await AsyncVirtualPropertyGraph(
|
||||
self._inner, name, namespace_path
|
||||
).describe()
|
||||
|
||||
async def list_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the virtual property graphs in one namespace."""
|
||||
return await self._inner.list_graphs(
|
||||
VIRTUAL_PROPERTY_GRAPH, list(namespace_path or [])
|
||||
)
|
||||
|
||||
async def drop_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a virtual property graph and wait for its definition to be
|
||||
deleted."""
|
||||
await self._inner.drop_graph(
|
||||
VIRTUAL_PROPERTY_GRAPH, name, list(namespace_path or [])
|
||||
)
|
||||
|
||||
async def create_materialized_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> AsyncMaterializedVirtualPropertyGraph:
|
||||
"""Create a materialized virtual property graph, built from its tables
|
||||
now.
|
||||
|
||||
See
|
||||
[DBConnection.create_materialized_virtual_property_graph][lancedb.DBConnection.create_materialized_virtual_property_graph].
|
||||
"""
|
||||
graph = AsyncMaterializedVirtualPropertyGraph(self._inner, name, namespace_path)
|
||||
await self._inner.create_graph(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
_tables_json(nodes, edges),
|
||||
graph.namespace_path,
|
||||
)
|
||||
return graph
|
||||
|
||||
async def open_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> AsyncMaterializedVirtualPropertyGraph:
|
||||
"""Open the materialized virtual property graph named ``name``; an error
|
||||
if there is none."""
|
||||
graph = AsyncMaterializedVirtualPropertyGraph(self._inner, name, namespace_path)
|
||||
await graph.describe()
|
||||
return graph
|
||||
|
||||
async def describe_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""What this database records about a materialized virtual property
|
||||
graph."""
|
||||
return await AsyncMaterializedVirtualPropertyGraph(
|
||||
self._inner, name, namespace_path
|
||||
).describe()
|
||||
|
||||
async def list_materialized_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
"""The names of the materialized virtual property graphs in one
|
||||
namespace."""
|
||||
return await self._inner.list_graphs(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH, list(namespace_path or [])
|
||||
)
|
||||
|
||||
async def drop_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Drop a materialized virtual property graph and wait for its data to
|
||||
be deleted."""
|
||||
await self._inner.drop_graph(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH, name, list(namespace_path or [])
|
||||
)
|
||||
|
||||
async def list_jobs(self) -> List[JobInfo]:
|
||||
"""List server-side jobs across the database's tables."""
|
||||
|
||||
+342
-80
@@ -1,31 +1,46 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
"""Property graphs: catalog objects read as graphs, in one of two modes.
|
||||
"""Property graphs: catalog objects read as graphs, in three kinds that share
|
||||
one namespace of names.
|
||||
|
||||
An *independent* graph holds its own rows. Its node types (:class:`NodeType`)
|
||||
and edge types (:class:`EdgeType`) have schemas, and rows are inserted into the
|
||||
graph itself with ``insert_into_property_graph``.
|
||||
A *property graph* holds its own rows. Its node types (:class:`NodeType`) and
|
||||
edge types (:class:`EdgeType`) have schemas, and rows are inserted into the
|
||||
graph itself with :meth:`PropertyGraph.insert`.
|
||||
|
||||
A *materialized view* graph reads tables in its namespace. Its definition says
|
||||
A *virtual property graph* reads tables in its namespace. Its definition says
|
||||
which tables hold nodes and which hold edges: each node table's key and label
|
||||
(:class:`NodeTable`), each edge table's label and how its source and
|
||||
destination columns reference node keys (:class:`EdgeTable`) -- the shape of
|
||||
SQL/PGQ's ``CREATE PROPERTY GRAPH``. The tables stay ordinary tables; after
|
||||
they change, ``refresh_property_graph`` brings the graph up to date.
|
||||
SQL/PGQ's ``CREATE PROPERTY GRAPH``. A query reads the tables as they are.
|
||||
|
||||
See ``DBConnection.create_property_graph``.
|
||||
A *materialized virtual property graph* has a virtual graph's definition, and
|
||||
is read as its last refresh built it: after the tables change,
|
||||
:meth:`MaterializedVirtualPropertyGraph.refresh` brings it up to date.
|
||||
|
||||
See ``DBConnection.create_property_graph``,
|
||||
``DBConnection.create_virtual_property_graph`` and
|
||||
``DBConnection.create_materialized_virtual_property_graph``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Type
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
from ._lancedb import graph_schema_from_json, graph_schema_to_json
|
||||
from .background_loop import LOOP
|
||||
from .scannable import to_scannable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .common import DATA
|
||||
|
||||
PROPERTY_GRAPH = "property_graph"
|
||||
VIRTUAL_PROPERTY_GRAPH = "virtual_property_graph"
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH = "materialized_virtual_property_graph"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -72,7 +87,7 @@ class EdgeTable:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NodeType:
|
||||
"""The nodes of one label of an independent graph."""
|
||||
"""The nodes of one label of a property graph."""
|
||||
|
||||
label: str
|
||||
"""The label its nodes carry."""
|
||||
@@ -85,7 +100,7 @@ class NodeType:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EdgeType:
|
||||
"""The edges of one label of an independent graph."""
|
||||
"""The edges of one label of a property graph."""
|
||||
|
||||
label: str
|
||||
"""The label its edges carry."""
|
||||
@@ -107,12 +122,10 @@ class PropertyGraphDescription:
|
||||
"""The graph's name within its namespace."""
|
||||
namespace_path: List[str]
|
||||
"""The namespace holding the graph; empty is the root namespace."""
|
||||
mode: str
|
||||
"""``"independent"`` or ``"materialized_view"``."""
|
||||
nodes: List[Union[NodeTable, NodeType]]
|
||||
"""The node tables (materialized view) or node types (independent)."""
|
||||
edges: List[Union[EdgeTable, EdgeType]]
|
||||
"""The edge tables (materialized view) or edge types (independent)."""
|
||||
nodes: List[NodeType]
|
||||
"""The node types."""
|
||||
edges: List[EdgeType]
|
||||
"""The edge types."""
|
||||
commit: str
|
||||
"""The graph's current commit."""
|
||||
committed_at: str
|
||||
@@ -123,52 +136,120 @@ class PropertyGraphDescription:
|
||||
"""How many nodes the current commit holds."""
|
||||
edge_count: int
|
||||
"""How many edges the current commit holds."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VirtualPropertyGraphDescription:
|
||||
"""What a database records about one virtual property graph."""
|
||||
|
||||
name: str
|
||||
"""The graph's name within its namespace."""
|
||||
namespace_path: List[str]
|
||||
"""The namespace holding the graph; empty is the root namespace."""
|
||||
nodes: List[NodeTable]
|
||||
"""The node tables, each with the properties create resolved."""
|
||||
edges: List[EdgeTable]
|
||||
"""The edge tables, each with the properties create resolved."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MaterializedVirtualPropertyGraphDescription:
|
||||
"""What a database records about one materialized virtual property graph."""
|
||||
|
||||
name: str
|
||||
"""The graph's name within its namespace."""
|
||||
namespace_path: List[str]
|
||||
"""The namespace holding the graph; empty is the root namespace."""
|
||||
nodes: List[NodeTable]
|
||||
"""The node tables, each with the properties create resolved."""
|
||||
edges: List[EdgeTable]
|
||||
"""The edge tables, each with the properties create resolved."""
|
||||
commit: str
|
||||
"""The last refresh's commit."""
|
||||
committed_at: str
|
||||
"""When the last refresh landed, as RFC 3339."""
|
||||
vertex_count: int
|
||||
"""How many nodes the last refresh built."""
|
||||
edge_count: int
|
||||
"""How many edges the last refresh built."""
|
||||
sources: Dict[str, int]
|
||||
"""For a materialized view, the version of each table the current commit
|
||||
was refreshed from, by table name."""
|
||||
"""The version of each table the last refresh read, by table name."""
|
||||
|
||||
|
||||
INDEPENDENT = "independent"
|
||||
MATERIALIZED_VIEW = "materialized_view"
|
||||
def _require(
|
||||
kind: str,
|
||||
nodes: Sequence[Any],
|
||||
edges: Sequence[Any],
|
||||
node_type: Type,
|
||||
edge_type: Type,
|
||||
) -> None:
|
||||
for element, expected in [
|
||||
*((node, node_type) for node in nodes),
|
||||
*((edge, edge_type) for edge in edges),
|
||||
]:
|
||||
if not isinstance(element, expected):
|
||||
raise TypeError(
|
||||
f"A {kind}'s nodes are {node_type.__name__} and its edges "
|
||||
f"{edge_type.__name__}, not {type(element).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _mode_of(nodes: Sequence[Any], edges: Sequence[Any]) -> str:
|
||||
kinds = {type(element) for element in [*nodes, *edges]}
|
||||
if kinds <= {NodeType, EdgeType} and kinds:
|
||||
return INDEPENDENT
|
||||
if kinds <= {NodeTable, EdgeTable}:
|
||||
return MATERIALIZED_VIEW
|
||||
raise ValueError(
|
||||
"A property graph's nodes and edges are either all tables "
|
||||
"(NodeTable, EdgeTable) or all types (NodeType, EdgeType)"
|
||||
)
|
||||
|
||||
|
||||
def _definition_json(nodes: Sequence[Any], edges: Sequence[Any]) -> str:
|
||||
mode = _mode_of(nodes, edges)
|
||||
def _types_json(nodes: Sequence[NodeType], edges: Sequence[EdgeType]) -> str:
|
||||
_require("property graph", nodes, edges, NodeType, EdgeType)
|
||||
return json.dumps(
|
||||
{
|
||||
"mode": mode,
|
||||
"nodes": [_node_to_json(node) for node in nodes],
|
||||
"edges": [_edge_to_json(edge) for edge in edges],
|
||||
"nodes": [_node_type_to_json(node) for node in nodes],
|
||||
"edges": [_edge_type_to_json(edge) for edge in edges],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _description_from_json(text: str) -> PropertyGraphDescription:
|
||||
def _tables_json(nodes: Sequence[NodeTable], edges: Sequence[EdgeTable]) -> str:
|
||||
_require("virtual property graph", nodes, edges, NodeTable, EdgeTable)
|
||||
return json.dumps(
|
||||
{
|
||||
"nodes": [_node_table_to_json(node) for node in nodes],
|
||||
"edges": [_edge_table_to_json(edge) for edge in edges],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _property_description(text: str) -> PropertyGraphDescription:
|
||||
described = json.loads(text)
|
||||
mode = described["mode"]
|
||||
return PropertyGraphDescription(
|
||||
name=described["name"],
|
||||
namespace_path=list(described.get("namespace", [])),
|
||||
mode=mode,
|
||||
nodes=[_node_from_json(mode, node) for node in described["nodes"]],
|
||||
edges=[_edge_from_json(mode, edge) for edge in described.get("edges", [])],
|
||||
nodes=[_node_type_from_json(node) for node in described["nodes"]],
|
||||
edges=[_edge_type_from_json(edge) for edge in described.get("edges", [])],
|
||||
commit=described["commit"],
|
||||
committed_at=described["committed_at"],
|
||||
previous_commit=described.get("previous_commit"),
|
||||
vertex_count=described["vertex_count"],
|
||||
edge_count=described["edge_count"],
|
||||
)
|
||||
|
||||
|
||||
def _virtual_description(text: str) -> VirtualPropertyGraphDescription:
|
||||
described = json.loads(text)
|
||||
return VirtualPropertyGraphDescription(
|
||||
name=described["name"],
|
||||
namespace_path=list(described.get("namespace", [])),
|
||||
nodes=[_node_table_from_json(node) for node in described["nodes"]],
|
||||
edges=[_edge_table_from_json(edge) for edge in described.get("edges", [])],
|
||||
)
|
||||
|
||||
|
||||
def _materialized_description(text: str) -> MaterializedVirtualPropertyGraphDescription:
|
||||
described = json.loads(text)
|
||||
return MaterializedVirtualPropertyGraphDescription(
|
||||
name=described["name"],
|
||||
namespace_path=list(described.get("namespace", [])),
|
||||
nodes=[_node_table_from_json(node) for node in described["nodes"]],
|
||||
edges=[_edge_table_from_json(edge) for edge in described.get("edges", [])],
|
||||
commit=described["commit"],
|
||||
committed_at=described["committed_at"],
|
||||
vertex_count=described["vertex_count"],
|
||||
edge_count=described["edge_count"],
|
||||
sources={
|
||||
source["table"]: source["version"]
|
||||
for source in described.get("sources", [])
|
||||
@@ -184,13 +265,26 @@ def _schema_from_json(schema: Dict[str, Any]) -> pa.Schema:
|
||||
return graph_schema_from_json(json.dumps(schema))
|
||||
|
||||
|
||||
def _node_to_json(node: Union[NodeTable, NodeType]) -> Dict[str, Any]:
|
||||
if isinstance(node, NodeType):
|
||||
return {
|
||||
"label": node.label,
|
||||
"key": node.key,
|
||||
"schema": _schema_to_json(node.schema),
|
||||
}
|
||||
def _node_type_to_json(node: NodeType) -> Dict[str, Any]:
|
||||
return {
|
||||
"label": node.label,
|
||||
"key": node.key,
|
||||
"schema": _schema_to_json(node.schema),
|
||||
}
|
||||
|
||||
|
||||
def _edge_type_to_json(edge: EdgeType) -> Dict[str, Any]:
|
||||
encoded: Dict[str, Any] = {
|
||||
"label": edge.label,
|
||||
"source": {"label": edge.source[0], "column": edge.source[1]},
|
||||
"destination": {"label": edge.destination[0], "column": edge.destination[1]},
|
||||
}
|
||||
if edge.schema is not None:
|
||||
encoded["schema"] = _schema_to_json(edge.schema)
|
||||
return encoded
|
||||
|
||||
|
||||
def _node_table_to_json(node: NodeTable) -> Dict[str, Any]:
|
||||
encoded: Dict[str, Any] = {
|
||||
"table": node.table,
|
||||
"key": node.key,
|
||||
@@ -201,20 +295,8 @@ def _node_to_json(node: Union[NodeTable, NodeType]) -> Dict[str, Any]:
|
||||
return encoded
|
||||
|
||||
|
||||
def _edge_to_json(edge: Union[EdgeTable, EdgeType]) -> Dict[str, Any]:
|
||||
if isinstance(edge, EdgeType):
|
||||
encoded: Dict[str, Any] = {
|
||||
"label": edge.label,
|
||||
"source": {"label": edge.source[0], "column": edge.source[1]},
|
||||
"destination": {
|
||||
"label": edge.destination[0],
|
||||
"column": edge.destination[1],
|
||||
},
|
||||
}
|
||||
if edge.schema is not None:
|
||||
encoded["schema"] = _schema_to_json(edge.schema)
|
||||
return encoded
|
||||
encoded = {
|
||||
def _edge_table_to_json(edge: EdgeTable) -> Dict[str, Any]:
|
||||
encoded: Dict[str, Any] = {
|
||||
"table": edge.table,
|
||||
"label": edge.label,
|
||||
"source": _endpoint_to_json(edge.source),
|
||||
@@ -233,13 +315,23 @@ def _endpoint_to_json(endpoint: Endpoint) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _node_from_json(mode: str, node: Dict[str, Any]) -> Union[NodeTable, NodeType]:
|
||||
if mode == INDEPENDENT:
|
||||
return NodeType(
|
||||
label=node["label"],
|
||||
key=node["key"],
|
||||
schema=_schema_from_json(node["schema"]),
|
||||
)
|
||||
def _node_type_from_json(node: Dict[str, Any]) -> NodeType:
|
||||
return NodeType(
|
||||
label=node["label"], key=node["key"], schema=_schema_from_json(node["schema"])
|
||||
)
|
||||
|
||||
|
||||
def _edge_type_from_json(edge: Dict[str, Any]) -> EdgeType:
|
||||
schema = edge.get("schema")
|
||||
return EdgeType(
|
||||
label=edge["label"],
|
||||
source=(edge["source"]["label"], edge["source"]["column"]),
|
||||
destination=(edge["destination"]["label"], edge["destination"]["column"]),
|
||||
schema=None if schema is None else _schema_from_json(schema),
|
||||
)
|
||||
|
||||
|
||||
def _node_table_from_json(node: Dict[str, Any]) -> NodeTable:
|
||||
return NodeTable(
|
||||
table=node["table"],
|
||||
key=node["key"],
|
||||
@@ -248,15 +340,7 @@ def _node_from_json(mode: str, node: Dict[str, Any]) -> Union[NodeTable, NodeTyp
|
||||
)
|
||||
|
||||
|
||||
def _edge_from_json(mode: str, edge: Dict[str, Any]) -> Union[EdgeTable, EdgeType]:
|
||||
if mode == INDEPENDENT:
|
||||
schema = edge.get("schema")
|
||||
return EdgeType(
|
||||
label=edge["label"],
|
||||
source=(edge["source"]["label"], edge["source"]["column"]),
|
||||
destination=(edge["destination"]["label"], edge["destination"]["column"]),
|
||||
schema=None if schema is None else _schema_from_json(schema),
|
||||
)
|
||||
def _edge_table_from_json(edge: Dict[str, Any]) -> EdgeTable:
|
||||
return EdgeTable(
|
||||
table=edge["table"],
|
||||
label=edge["label"],
|
||||
@@ -272,3 +356,181 @@ def _endpoint_from_json(endpoint: Dict[str, Any]) -> Endpoint:
|
||||
column=endpoint["column"],
|
||||
references=(references["table"], references["column"]),
|
||||
)
|
||||
|
||||
|
||||
class _AsyncGraph:
|
||||
"""A handle on one graph of a database: its name and namespace, and the
|
||||
connection it is read and written through."""
|
||||
|
||||
_kind: str = PROPERTY_GRAPH
|
||||
|
||||
def __init__(self, inner: Any, name: str, namespace_path: Optional[List[str]]):
|
||||
self._inner = inner
|
||||
self._name = name
|
||||
self._namespace_path = list(namespace_path or [])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}(name={self._name!r})"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The graph's name within its namespace."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def namespace_path(self) -> List[str]:
|
||||
"""The namespace holding the graph; empty is the root namespace."""
|
||||
return list(self._namespace_path)
|
||||
|
||||
async def _describe(self) -> str:
|
||||
return await self._inner.describe_graph(
|
||||
self._kind, self._name, self._namespace_path
|
||||
)
|
||||
|
||||
|
||||
class AsyncPropertyGraph(_AsyncGraph):
|
||||
"""A property graph: one that holds its own rows.
|
||||
|
||||
Obtained from ``AsyncConnection.create_property_graph`` or
|
||||
``AsyncConnection.open_property_graph``.
|
||||
"""
|
||||
|
||||
_kind = PROPERTY_GRAPH
|
||||
|
||||
async def describe(self) -> PropertyGraphDescription:
|
||||
"""The graph's node and edge types, current commit and size."""
|
||||
return _property_description(await self._describe())
|
||||
|
||||
async def insert(self, label: str, data: "DATA") -> PropertyGraphDescription:
|
||||
"""Insert rows of one node label or edge label, as one commit.
|
||||
|
||||
Nodes upsert by key: a key the graph holds replaces that node's
|
||||
properties. Edges append, carrying their endpoints' keys in the columns
|
||||
the edge type names; an edge whose endpoint is not a node of the graph
|
||||
refuses the whole insert.
|
||||
"""
|
||||
return _property_description(
|
||||
await self._inner.insert_into_property_graph(
|
||||
self._name, label, to_scannable(data), self._namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
async def rollback(self) -> PropertyGraphDescription:
|
||||
"""Return the graph to its previous commit.
|
||||
|
||||
The commit rolled back from is discarded, so a second rollback in a row
|
||||
is an error.
|
||||
"""
|
||||
return _property_description(
|
||||
await self._inner.rollback_property_graph(self._name, self._namespace_path)
|
||||
)
|
||||
|
||||
|
||||
class AsyncVirtualPropertyGraph(_AsyncGraph):
|
||||
"""A virtual property graph: one defined over tables and read through them.
|
||||
|
||||
Obtained from ``AsyncConnection.create_virtual_property_graph`` or
|
||||
``AsyncConnection.open_virtual_property_graph``.
|
||||
"""
|
||||
|
||||
_kind = VIRTUAL_PROPERTY_GRAPH
|
||||
|
||||
async def describe(self) -> VirtualPropertyGraphDescription:
|
||||
"""The graph's node and edge tables."""
|
||||
return _virtual_description(await self._describe())
|
||||
|
||||
|
||||
class AsyncMaterializedVirtualPropertyGraph(_AsyncGraph):
|
||||
"""A materialized virtual property graph: one defined over tables and read
|
||||
as its last refresh built it.
|
||||
|
||||
Obtained from ``AsyncConnection.create_materialized_virtual_property_graph``
|
||||
or ``AsyncConnection.open_materialized_virtual_property_graph``.
|
||||
"""
|
||||
|
||||
_kind = MATERIALIZED_VIRTUAL_PROPERTY_GRAPH
|
||||
|
||||
async def describe(self) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""The graph's node and edge tables, and what its last refresh built."""
|
||||
return _materialized_description(await self._describe())
|
||||
|
||||
async def refresh(self) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""Rebuild the graph from its tables' latest versions.
|
||||
|
||||
When every table's commits since the last refresh left its rows as
|
||||
they were -- compaction, indexing, metadata -- only the recorded
|
||||
versions advance; when nothing changed, the commit stays.
|
||||
"""
|
||||
return _materialized_description(
|
||||
await self._inner.refresh_materialized_virtual_property_graph(
|
||||
self._name, self._namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class _Graph:
|
||||
"""Synchronous variant of a graph handle."""
|
||||
|
||||
def __init__(self, graph: _AsyncGraph):
|
||||
self._async = graph
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}(name={self.name!r})"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The graph's name within its namespace."""
|
||||
return self._async.name
|
||||
|
||||
@property
|
||||
def namespace_path(self) -> List[str]:
|
||||
"""The namespace holding the graph; empty is the root namespace."""
|
||||
return self._async.namespace_path
|
||||
|
||||
|
||||
class PropertyGraph(_Graph):
|
||||
"""Synchronous variant of
|
||||
[AsyncPropertyGraph][lancedb.graph.AsyncPropertyGraph]."""
|
||||
|
||||
_async: AsyncPropertyGraph
|
||||
|
||||
def describe(self) -> PropertyGraphDescription:
|
||||
"""The graph's node and edge types, current commit and size."""
|
||||
return LOOP.run(self._async.describe())
|
||||
|
||||
def insert(self, label: str, data: "DATA") -> PropertyGraphDescription:
|
||||
"""Insert rows of one node label or edge label, as one commit. See
|
||||
[AsyncPropertyGraph.insert][lancedb.graph.AsyncPropertyGraph.insert]."""
|
||||
return LOOP.run(self._async.insert(label, data))
|
||||
|
||||
def rollback(self) -> PropertyGraphDescription:
|
||||
"""Return the graph to its previous commit. See
|
||||
[AsyncPropertyGraph.rollback][lancedb.graph.AsyncPropertyGraph.rollback]."""
|
||||
return LOOP.run(self._async.rollback())
|
||||
|
||||
|
||||
class VirtualPropertyGraph(_Graph):
|
||||
"""Synchronous variant of
|
||||
[AsyncVirtualPropertyGraph][lancedb.graph.AsyncVirtualPropertyGraph]."""
|
||||
|
||||
_async: AsyncVirtualPropertyGraph
|
||||
|
||||
def describe(self) -> VirtualPropertyGraphDescription:
|
||||
"""The graph's node and edge tables."""
|
||||
return LOOP.run(self._async.describe())
|
||||
|
||||
|
||||
class MaterializedVirtualPropertyGraph(_Graph):
|
||||
"""Synchronous variant of
|
||||
[AsyncMaterializedVirtualPropertyGraph][lancedb.graph.AsyncMaterializedVirtualPropertyGraph]."""
|
||||
|
||||
_async: AsyncMaterializedVirtualPropertyGraph
|
||||
|
||||
def describe(self) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""The graph's node and edge tables, and what its last refresh built."""
|
||||
return LOOP.run(self._async.describe())
|
||||
|
||||
def refresh(self) -> MaterializedVirtualPropertyGraphDescription:
|
||||
"""Rebuild the graph from its tables' latest versions. See
|
||||
[AsyncMaterializedVirtualPropertyGraph.refresh][lancedb.graph.AsyncMaterializedVirtualPropertyGraph.refresh]."""
|
||||
return LOOP.run(self._async.refresh())
|
||||
|
||||
@@ -36,7 +36,18 @@ import pyarrow as pa
|
||||
from ..common import DATA
|
||||
from ..db import DBConnection, LOOP
|
||||
from ..functions import FunctionVersion, UdfDefinition
|
||||
from ..graph import EdgeTable, EdgeType, NodeTable, NodeType, PropertyGraphDescription
|
||||
from ..graph import (
|
||||
EdgeTable,
|
||||
EdgeType,
|
||||
MaterializedVirtualPropertyGraph,
|
||||
MaterializedVirtualPropertyGraphDescription,
|
||||
NodeTable,
|
||||
NodeType,
|
||||
PropertyGraph,
|
||||
PropertyGraphDescription,
|
||||
VirtualPropertyGraph,
|
||||
VirtualPropertyGraphDescription,
|
||||
)
|
||||
from ..job import AsyncJob, Job
|
||||
from ..sql import Query as SqlQuery
|
||||
from ..sql import QueryDescription
|
||||
@@ -947,14 +958,26 @@ class RemoteDBConnection(DBConnection):
|
||||
def create_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[Union[NodeTable, NodeType]],
|
||||
edges: List[Union[EdgeTable, EdgeType]],
|
||||
nodes: List[NodeType],
|
||||
edges: List[EdgeType],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.create_property_graph(
|
||||
name, nodes, edges, namespace_path=namespace_path
|
||||
) -> PropertyGraph:
|
||||
return PropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.create_property_graph(
|
||||
name, nodes, edges, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def open_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraph:
|
||||
return PropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.open_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -967,35 +990,10 @@ class RemoteDBConnection(DBConnection):
|
||||
)
|
||||
|
||||
@override
|
||||
def insert_into_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
label: str,
|
||||
data: DATA,
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.insert_into_property_graph(
|
||||
name, label, data, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def refresh_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.refresh_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def rollback_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> PropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.rollback_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
def list_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
return LOOP.run(self._conn.list_property_graphs(namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def drop_property_graph(
|
||||
@@ -1004,10 +1002,118 @@ class RemoteDBConnection(DBConnection):
|
||||
LOOP.run(self._conn.drop_property_graph(name, namespace_path=namespace_path))
|
||||
|
||||
@override
|
||||
def list_property_graphs(
|
||||
def create_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> VirtualPropertyGraph:
|
||||
return VirtualPropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.create_virtual_property_graph(
|
||||
name, nodes, edges, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def open_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> VirtualPropertyGraph:
|
||||
return VirtualPropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.open_virtual_property_graph(
|
||||
name, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def describe_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> VirtualPropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.describe_virtual_property_graph(
|
||||
name, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def list_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
return LOOP.run(self._conn.list_property_graphs(namespace_path=namespace_path))
|
||||
return LOOP.run(
|
||||
self._conn.list_virtual_property_graphs(namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def drop_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
LOOP.run(
|
||||
self._conn.drop_virtual_property_graph(name, namespace_path=namespace_path)
|
||||
)
|
||||
|
||||
@override
|
||||
def create_materialized_virtual_property_graph(
|
||||
self,
|
||||
name: str,
|
||||
nodes: List[NodeTable],
|
||||
edges: List[EdgeTable],
|
||||
*,
|
||||
namespace_path: Optional[List[str]] = None,
|
||||
) -> MaterializedVirtualPropertyGraph:
|
||||
return MaterializedVirtualPropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.create_materialized_virtual_property_graph(
|
||||
name, nodes, edges, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def open_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> MaterializedVirtualPropertyGraph:
|
||||
return MaterializedVirtualPropertyGraph(
|
||||
LOOP.run(
|
||||
self._conn.open_materialized_virtual_property_graph(
|
||||
name, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def describe_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> MaterializedVirtualPropertyGraphDescription:
|
||||
return LOOP.run(
|
||||
self._conn.describe_materialized_virtual_property_graph(
|
||||
name, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def list_materialized_virtual_property_graphs(
|
||||
self, *, namespace_path: Optional[List[str]] = None
|
||||
) -> List[str]:
|
||||
return LOOP.run(
|
||||
self._conn.list_materialized_virtual_property_graphs(
|
||||
namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def drop_materialized_virtual_property_graph(
|
||||
self, name: str, *, namespace_path: Optional[List[str]] = None
|
||||
) -> None:
|
||||
LOOP.run(
|
||||
self._conn.drop_materialized_virtual_property_graph(
|
||||
name, namespace_path=namespace_path
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def list_jobs(self) -> List["JobInfo"]:
|
||||
|
||||
@@ -21,9 +21,13 @@ from lancedb.graph import (
|
||||
EdgeTable,
|
||||
EdgeType,
|
||||
Endpoint,
|
||||
MaterializedVirtualPropertyGraph,
|
||||
MaterializedVirtualPropertyGraphDescription,
|
||||
NodeTable,
|
||||
NodeType,
|
||||
PropertyGraphDescription,
|
||||
PropertyGraph,
|
||||
VirtualPropertyGraph,
|
||||
VirtualPropertyGraphDescription,
|
||||
)
|
||||
from lancedb.query import ColumnOrdering
|
||||
from lancedb.remote import ClientConfig
|
||||
@@ -2838,35 +2842,68 @@ def test_view_crud_addresses_its_own_routes():
|
||||
]
|
||||
|
||||
|
||||
def test_property_graph_crud_addresses_its_own_routes():
|
||||
# The graph verbs are their own routes: the definition is the create body,
|
||||
# and a description adds the graph's commits, its size and the table
|
||||
# versions it was refreshed from.
|
||||
def _graph_handler(calls, respond):
|
||||
"""A mock route handler recording `(method, path, body)` and answering
|
||||
with `respond(path, body)` as JSON."""
|
||||
|
||||
def handler(request):
|
||||
length = int(request.headers.get("Content-Length") or 0)
|
||||
raw = request.rfile.read(length) if length else b""
|
||||
if request.path.split("?")[0].endswith("/insert"):
|
||||
body = pa.ipc.open_stream(raw).read_all().to_pylist()
|
||||
else:
|
||||
body = json.loads(raw) if raw else None
|
||||
calls.append((request.command, request.path, body))
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(json.dumps(respond(request.path, body)).encode())
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
SOCIAL_NODES = [
|
||||
NodeTable("person", key="person_id", label="Person", properties=["name"])
|
||||
]
|
||||
SOCIAL_EDGES = [
|
||||
EdgeTable(
|
||||
"knows",
|
||||
label="KNOWS",
|
||||
source=Endpoint("src_id", references=("person", "person_id")),
|
||||
destination=Endpoint("dst_id", references=("person", "person_id")),
|
||||
)
|
||||
]
|
||||
SOCIAL_DEFINITION = {
|
||||
"nodes": [
|
||||
{
|
||||
"table": "person",
|
||||
"key": "person_id",
|
||||
"label": "Person",
|
||||
"properties": ["name"],
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"table": "knows",
|
||||
"label": "KNOWS",
|
||||
"source": {
|
||||
"column": "src_id",
|
||||
"references": {"table": "person", "column": "person_id"},
|
||||
},
|
||||
"destination": {
|
||||
"column": "dst_id",
|
||||
"references": {"table": "person", "column": "person_id"},
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_virtual_and_materialized_graphs_address_their_own_routes():
|
||||
# Each kind of graph has its own routes; the definition is the create
|
||||
# body, and a materialized graph's description adds its last refresh.
|
||||
calls = []
|
||||
person = {"table": "person", "column": "person_id"}
|
||||
definition = {
|
||||
"mode": "materialized_view",
|
||||
"nodes": [
|
||||
{
|
||||
"table": "person",
|
||||
"key": "person_id",
|
||||
"label": "Person",
|
||||
"properties": ["name"],
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"table": "knows",
|
||||
"label": "KNOWS",
|
||||
"source": {"column": "src_id", "references": person},
|
||||
"destination": {"column": "dst_id", "references": person},
|
||||
}
|
||||
],
|
||||
}
|
||||
described = {
|
||||
"name": "social",
|
||||
"namespace": ["analytics"],
|
||||
**definition,
|
||||
refreshed = {
|
||||
"commit": "6f1c2d3e-0000-4000-8000-000000000000",
|
||||
"committed_at": "2026-09-26T17:00:00Z",
|
||||
"vertex_count": 4,
|
||||
@@ -2877,62 +2914,106 @@ def test_property_graph_crud_addresses_its_own_routes():
|
||||
],
|
||||
}
|
||||
|
||||
def handler(request):
|
||||
length = int(request.headers.get("Content-Length") or 0)
|
||||
body = json.loads(request.rfile.read(length)) if length else None
|
||||
calls.append((request.command, request.path, body))
|
||||
if request.path.endswith("/property_graph/list"):
|
||||
response = {"property_graphs": ["social"], "page_token": None}
|
||||
elif request.path.endswith("/drop"):
|
||||
response = {}
|
||||
else:
|
||||
response = described
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(json.dumps(response).encode())
|
||||
def respond(path, body):
|
||||
if path.endswith("/virtual_property_graph/list"):
|
||||
return {"virtual_property_graphs": ["social"], "page_token": None}
|
||||
if path.endswith("/materialized_virtual_property_graph/list"):
|
||||
return {"materialized_virtual_property_graphs": ["social_mv"]}
|
||||
if path.endswith("/drop"):
|
||||
return {}
|
||||
name = path.split("$")[1].split("/")[0]
|
||||
described = {"name": name, "namespace": ["analytics"], **SOCIAL_DEFINITION}
|
||||
if "/materialized_virtual_property_graph/" in path:
|
||||
described.update(refreshed)
|
||||
return described
|
||||
|
||||
nodes = [NodeTable("person", key="person_id", label="Person", properties=["name"])]
|
||||
edges = [
|
||||
EdgeTable(
|
||||
"knows",
|
||||
label="KNOWS",
|
||||
source=Endpoint("src_id", references=("person", "person_id")),
|
||||
destination=Endpoint("dst_id", references=("person", "person_id")),
|
||||
with mock_lancedb_connection(_graph_handler(calls, respond)) as db:
|
||||
graph = db.create_virtual_property_graph(
|
||||
"social", SOCIAL_NODES, SOCIAL_EDGES, namespace_path=["analytics"]
|
||||
)
|
||||
]
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
graph = db.create_property_graph(
|
||||
"social", nodes, edges, namespace_path=["analytics"]
|
||||
)
|
||||
assert graph == PropertyGraphDescription(
|
||||
assert isinstance(graph, VirtualPropertyGraph)
|
||||
assert (graph.name, graph.namespace_path) == ("social", ["analytics"])
|
||||
assert graph.describe() == VirtualPropertyGraphDescription(
|
||||
name="social",
|
||||
namespace_path=["analytics"],
|
||||
mode="materialized_view",
|
||||
nodes=nodes,
|
||||
edges=edges,
|
||||
nodes=SOCIAL_NODES,
|
||||
edges=SOCIAL_EDGES,
|
||||
)
|
||||
assert db.list_virtual_property_graphs(namespace_path=["analytics"]) == [
|
||||
"social"
|
||||
]
|
||||
db.drop_virtual_property_graph("social", namespace_path=["analytics"])
|
||||
|
||||
mvpg = db.create_materialized_virtual_property_graph(
|
||||
"social_mv", SOCIAL_NODES, SOCIAL_EDGES, namespace_path=["analytics"]
|
||||
)
|
||||
assert isinstance(mvpg, MaterializedVirtualPropertyGraph)
|
||||
expected = MaterializedVirtualPropertyGraphDescription(
|
||||
name="social_mv",
|
||||
namespace_path=["analytics"],
|
||||
nodes=SOCIAL_NODES,
|
||||
edges=SOCIAL_EDGES,
|
||||
commit="6f1c2d3e-0000-4000-8000-000000000000",
|
||||
committed_at="2026-09-26T17:00:00Z",
|
||||
previous_commit=None,
|
||||
vertex_count=4,
|
||||
edge_count=5,
|
||||
sources={"person": 3, "knows": 2},
|
||||
)
|
||||
assert db.describe_property_graph("social", namespace_path=["analytics"]) == (
|
||||
graph
|
||||
assert mvpg.refresh() == expected
|
||||
opened = db.open_materialized_virtual_property_graph(
|
||||
"social_mv", namespace_path=["analytics"]
|
||||
)
|
||||
assert opened.describe() == expected
|
||||
assert db.list_materialized_virtual_property_graphs(
|
||||
namespace_path=["analytics"]
|
||||
) == ["social_mv"]
|
||||
db.drop_materialized_virtual_property_graph(
|
||||
"social_mv", namespace_path=["analytics"]
|
||||
)
|
||||
assert db.list_property_graphs(namespace_path=["analytics"]) == ["social"]
|
||||
db.drop_property_graph("social", namespace_path=["analytics"])
|
||||
|
||||
assert calls == [
|
||||
("POST", "/v1/property_graph/analytics$social/create", definition),
|
||||
("POST", "/v1/property_graph/analytics$social/describe", None),
|
||||
("GET", "/v1/namespace/analytics/property_graph/list", None),
|
||||
("POST", "/v1/property_graph/analytics$social/drop", None),
|
||||
(
|
||||
"POST",
|
||||
"/v1/virtual_property_graph/analytics$social/create",
|
||||
SOCIAL_DEFINITION,
|
||||
),
|
||||
("POST", "/v1/virtual_property_graph/analytics$social/describe", None),
|
||||
("GET", "/v1/namespace/analytics/virtual_property_graph/list", None),
|
||||
("POST", "/v1/virtual_property_graph/analytics$social/drop", None),
|
||||
(
|
||||
"POST",
|
||||
"/v1/materialized_virtual_property_graph/analytics$social_mv/create",
|
||||
SOCIAL_DEFINITION,
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/v1/materialized_virtual_property_graph/analytics$social_mv/refresh",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/v1/materialized_virtual_property_graph/analytics$social_mv/describe",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/v1/materialized_virtual_property_graph/analytics$social_mv/describe",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/v1/namespace/analytics/materialized_virtual_property_graph/list",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"POST",
|
||||
"/v1/materialized_virtual_property_graph/analytics$social_mv/drop",
|
||||
None,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_an_independent_graph_carries_schemas_and_takes_arrow_rows():
|
||||
def test_a_property_graph_carries_schemas_and_takes_arrow_rows():
|
||||
# Node and edge types go up as JSON Arrow schemas and come back as pyarrow
|
||||
# schemas; inserted rows go up as an Arrow IPC stream.
|
||||
calls = []
|
||||
@@ -2954,17 +3035,12 @@ def test_an_independent_graph_carries_schemas_and_takes_arrow_rows():
|
||||
)
|
||||
created = {}
|
||||
|
||||
def handler(request):
|
||||
length = int(request.headers.get("Content-Length") or 0)
|
||||
raw = request.rfile.read(length) if length else b""
|
||||
if request.path.split("?")[0].endswith("/insert"):
|
||||
body = pa.ipc.open_stream(raw).read_all().to_pylist()
|
||||
else:
|
||||
body = json.loads(raw) if raw else None
|
||||
calls.append((request.command, request.path, body))
|
||||
if request.path.endswith("/create"):
|
||||
def respond(path, body):
|
||||
if path.endswith("/create"):
|
||||
created.update(body)
|
||||
response = {
|
||||
if path.endswith("/property_graph/list"):
|
||||
return {"property_graphs": ["people"]}
|
||||
return {
|
||||
"name": "people",
|
||||
"namespace": [],
|
||||
**created,
|
||||
@@ -2974,52 +3050,46 @@ def test_an_independent_graph_carries_schemas_and_takes_arrow_rows():
|
||||
"vertex_count": 1,
|
||||
"edge_count": 0,
|
||||
}
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(json.dumps(response).encode())
|
||||
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
with mock_lancedb_connection(_graph_handler(calls, respond)) as db:
|
||||
graph = db.create_property_graph("people", [person], [knows])
|
||||
assert graph.mode == "independent"
|
||||
assert graph.nodes == [person] and graph.edges == [knows]
|
||||
assert graph.previous_commit == "c1"
|
||||
inserted = db.insert_into_property_graph(
|
||||
"people", "Person", [{"person_id": 1, "tags": ["a"]}]
|
||||
)
|
||||
assert isinstance(graph, PropertyGraph)
|
||||
described = graph.describe()
|
||||
assert described.nodes == [person] and described.edges == [knows]
|
||||
assert described.previous_commit == "c1"
|
||||
inserted = graph.insert("Person", [{"person_id": 1, "tags": ["a"]}])
|
||||
assert inserted.vertex_count == 1
|
||||
db.refresh_property_graph("people")
|
||||
db.rollback_property_graph("people")
|
||||
graph.rollback()
|
||||
assert db.list_property_graphs() == ["people"]
|
||||
db.drop_property_graph("people")
|
||||
|
||||
create, insert, refresh, rollback = calls
|
||||
create, describe, insert, rollback, listed, drop = calls
|
||||
assert create[:2] == ("POST", "/v1/property_graph/people/create")
|
||||
assert create[2]["mode"] == "independent"
|
||||
assert "mode" not in create[2]
|
||||
assert create[2]["nodes"][0]["schema"]["fields"][0]["name"] == "person_id"
|
||||
assert create[2]["edges"][0]["source"] == {"label": "Person", "column": "src_id"}
|
||||
assert describe[:2] == ("POST", "/v1/property_graph/people/describe")
|
||||
assert insert == (
|
||||
"POST",
|
||||
"/v1/property_graph/people/insert?label=Person",
|
||||
[{"person_id": 1, "tags": ["a"]}],
|
||||
)
|
||||
assert refresh[:2] == ("POST", "/v1/property_graph/people/refresh")
|
||||
assert rollback[:2] == ("POST", "/v1/property_graph/people/rollback")
|
||||
assert listed[:2] == ("GET", "/v1/namespace/$/property_graph/list")
|
||||
assert drop[:2] == ("POST", "/v1/property_graph/people/drop")
|
||||
|
||||
with pytest.raises(ValueError, match="either all tables"):
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
db.create_property_graph(
|
||||
"mixed",
|
||||
[person],
|
||||
[
|
||||
EdgeTable(
|
||||
"knows",
|
||||
label="KNOWS",
|
||||
source=Endpoint("src_id", references=("person", "person_id")),
|
||||
destination=Endpoint(
|
||||
"dst_id", references=("person", "person_id")
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_a_graph_takes_only_its_own_kind_of_definition():
|
||||
person = NodeType(
|
||||
"Person",
|
||||
key="person_id",
|
||||
schema=pa.schema([pa.field("person_id", pa.int64(), nullable=False)]),
|
||||
)
|
||||
with mock_lancedb_connection(_graph_handler([], lambda path, body: {})) as db:
|
||||
with pytest.raises(TypeError, match="are NodeTable and its edges EdgeTable"):
|
||||
db.create_virtual_property_graph("mixed", [person], SOCIAL_EDGES)
|
||||
with pytest.raises(TypeError, match="NodeType"):
|
||||
db.create_property_graph("mixed", SOCIAL_NODES, [])
|
||||
|
||||
|
||||
def test_local_connections_refuse_property_graphs(tmp_path):
|
||||
@@ -3027,4 +3097,6 @@ def test_local_connections_refuse_property_graphs(tmp_path):
|
||||
with pytest.raises(NotImplementedError, match="Property graph operations"):
|
||||
db.list_property_graphs()
|
||||
with pytest.raises(NotImplementedError, match="Property graph operations"):
|
||||
db.describe_property_graph("social")
|
||||
db.describe_virtual_property_graph("social")
|
||||
with pytest.raises(NotImplementedError, match="Property graph operations"):
|
||||
db.open_materialized_virtual_property_graph("social")
|
||||
|
||||
+120
-48
@@ -9,6 +9,7 @@ use std::{
|
||||
|
||||
use crate::{
|
||||
error::PythonErrorExt,
|
||||
graph::GraphKind,
|
||||
namespace::{create_namespace_storage_options_provider, extract_namespace_arc},
|
||||
runtime::future_into_py,
|
||||
table::{Table, scannable::PyScannable},
|
||||
@@ -965,73 +966,144 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, definition_json, namespace_path=None))]
|
||||
pub fn create_property_graph(
|
||||
/// Create a graph of `kind` from its definition's JSON, and return its
|
||||
/// description's JSON.
|
||||
#[pyo3(signature = (kind, name, definition_json, namespace_path=None))]
|
||||
pub fn create_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
kind: String,
|
||||
name: String,
|
||||
definition_json: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
let definition =
|
||||
lancedb::graph::PropertyGraphDefinition::from_json(&definition_json).infer_error()?;
|
||||
let kind = GraphKind::parse(&kind)?;
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.create_property_graph(name, &definition, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
.infer_error()
|
||||
match kind {
|
||||
GraphKind::Property => {
|
||||
let definition =
|
||||
lancedb::graph::PropertyGraphDefinition::from_json(&definition_json)
|
||||
.infer_error()?;
|
||||
inner
|
||||
.create_property_graph(name, &definition, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
}
|
||||
GraphKind::Virtual => {
|
||||
let definition =
|
||||
lancedb::graph::VirtualPropertyGraphDefinition::from_json(&definition_json)
|
||||
.infer_error()?;
|
||||
inner
|
||||
.create_virtual_property_graph(name, &definition, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
}
|
||||
GraphKind::MaterializedVirtual => {
|
||||
let definition =
|
||||
lancedb::graph::VirtualPropertyGraphDefinition::from_json(&definition_json)
|
||||
.infer_error()?;
|
||||
inner
|
||||
.create_materialized_virtual_property_graph(
|
||||
name,
|
||||
&definition,
|
||||
&namespace_path,
|
||||
)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
}
|
||||
}
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, namespace_path=None))]
|
||||
pub fn describe_property_graph(
|
||||
/// The JSON description of the graph of `kind` named `name`.
|
||||
#[pyo3(signature = (kind, name, namespace_path=None))]
|
||||
pub fn describe_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
kind: String,
|
||||
name: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
let kind = GraphKind::parse(&kind)?;
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.describe_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
.infer_error()
|
||||
match kind {
|
||||
GraphKind::Property => inner
|
||||
.describe_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json(),
|
||||
GraphKind::Virtual => inner
|
||||
.describe_virtual_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json(),
|
||||
GraphKind::MaterializedVirtual => inner
|
||||
.describe_materialized_virtual_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json(),
|
||||
}
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, namespace_path=None))]
|
||||
pub fn drop_property_graph(
|
||||
/// Drop the graph of `kind` named `name`, waiting for its storage to be
|
||||
/// deleted.
|
||||
#[pyo3(signature = (kind, name, namespace_path=None))]
|
||||
pub fn drop_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
kind: String,
|
||||
name: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
let kind = GraphKind::parse(&kind)?;
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.drop_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()
|
||||
match kind {
|
||||
GraphKind::Property => inner.drop_property_graph(name, &namespace_path).await,
|
||||
GraphKind::Virtual => {
|
||||
inner
|
||||
.drop_virtual_property_graph(name, &namespace_path)
|
||||
.await
|
||||
}
|
||||
GraphKind::MaterializedVirtual => {
|
||||
inner
|
||||
.drop_materialized_virtual_property_graph(name, &namespace_path)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (namespace_path=None))]
|
||||
pub fn list_property_graphs(
|
||||
/// The names of the graphs of `kind` in one namespace.
|
||||
#[pyo3(signature = (kind, namespace_path=None))]
|
||||
pub fn list_graphs(
|
||||
self_: PyRef<'_, Self>,
|
||||
kind: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
let kind = GraphKind::parse(&kind)?;
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.list_property_graphs(&namespace_path)
|
||||
.await
|
||||
.infer_error()
|
||||
match kind {
|
||||
GraphKind::Property => inner.list_property_graphs(&namespace_path).await,
|
||||
GraphKind::Virtual => inner.list_virtual_property_graphs(&namespace_path).await,
|
||||
GraphKind::MaterializedVirtual => {
|
||||
inner
|
||||
.list_materialized_virtual_property_graphs(&namespace_path)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1055,24 +1127,6 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, namespace_path=None))]
|
||||
pub fn refresh_property_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.refresh_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, namespace_path=None))]
|
||||
pub fn rollback_property_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
@@ -1091,6 +1145,24 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (name, namespace_path=None))]
|
||||
pub fn refresh_materialized_virtual_property_graph(
|
||||
self_: PyRef<'_, Self>,
|
||||
name: String,
|
||||
namespace_path: Option<Vec<String>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
let namespace_path = namespace_path.unwrap_or_default();
|
||||
future_into_py(self_.py(), async move {
|
||||
inner
|
||||
.refresh_materialized_virtual_property_graph(name, &namespace_path)
|
||||
.await
|
||||
.infer_error()?
|
||||
.to_json()
|
||||
.infer_error()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_jobs(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.get_inner()?.clone();
|
||||
future_into_py(self_.py(), async move {
|
||||
|
||||
@@ -8,6 +8,27 @@ use arrow::{datatypes::Schema, pyarrow::PyArrowType};
|
||||
use lance_namespace::models::JsonArrowSchema;
|
||||
use pyo3::{PyResult, exceptions::PyValueError, pyfunction};
|
||||
|
||||
/// A kind of graph, spelled as its routes and catalog objects are.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GraphKind {
|
||||
Property,
|
||||
Virtual,
|
||||
MaterializedVirtual,
|
||||
}
|
||||
|
||||
impl GraphKind {
|
||||
pub fn parse(kind: &str) -> PyResult<Self> {
|
||||
match kind {
|
||||
"property_graph" => Ok(Self::Property),
|
||||
"virtual_property_graph" => Ok(Self::Virtual),
|
||||
"materialized_virtual_property_graph" => Ok(Self::MaterializedVirtual),
|
||||
other => Err(PyValueError::new_err(format!(
|
||||
"'{other}' is not a kind of property graph"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pyarrow schema as the JSON Arrow schema of a node or edge type.
|
||||
#[pyfunction]
|
||||
pub fn graph_schema_to_json(schema: PyArrowType<Schema>) -> PyResult<String> {
|
||||
|
||||
+187
-60
@@ -28,7 +28,10 @@ use crate::database::{
|
||||
};
|
||||
use crate::embeddings::{EmbeddingRegistry, MemoryRegistry};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{PropertyGraphDefinition, PropertyGraphDescription};
|
||||
use crate::graph::{
|
||||
MaterializedVirtualPropertyGraphDescription, PropertyGraphDefinition, PropertyGraphDescription,
|
||||
VirtualPropertyGraphDefinition, VirtualPropertyGraphDescription,
|
||||
};
|
||||
#[cfg(feature = "remote")]
|
||||
use crate::remote::{
|
||||
client::ClientConfig,
|
||||
@@ -867,40 +870,26 @@ impl Connection {
|
||||
self.internal.list_views(namespace_path).await
|
||||
}
|
||||
|
||||
/// Create a property graph.
|
||||
///
|
||||
/// A materialized view graph names the tables in its namespace that hold
|
||||
/// its nodes, with their keys and labels, and its edges, with the node keys
|
||||
/// their source and destination columns reference; it is built from them
|
||||
/// now. An independent graph names its node and edge types with their
|
||||
/// schemas, and starts empty. A name already taken is an error. Local
|
||||
/// databases return [`Error::NotSupported`].
|
||||
/// Create a property graph: a graph that holds its own rows, typed by its
|
||||
/// node and edge types, and starts empty. A name held by a graph of any
|
||||
/// kind is taken. Local databases return [`Error::NotSupported`].
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use lancedb::graph::{EdgeTable, Endpoint, EndpointReference, NodeTable,
|
||||
/// # PropertyGraphDefinition};
|
||||
/// # use arrow_schema::{DataType, Field, Schema};
|
||||
/// # use lancedb::graph::{EdgeType, EndpointType, NodeType, PropertyGraphDefinition};
|
||||
/// # async fn example(connection: &lancedb::Connection) -> lancedb::Result<()> {
|
||||
/// let person = EndpointReference {
|
||||
/// table: "person".to_string(),
|
||||
/// column: "person_id".to_string(),
|
||||
/// let person = NodeType::new(
|
||||
/// "Person",
|
||||
/// "person_id",
|
||||
/// &Schema::new(vec![Field::new("person_id", DataType::Int64, false)]),
|
||||
/// )?;
|
||||
/// let endpoint = |column: &str| EndpointType {
|
||||
/// label: "Person".to_string(),
|
||||
/// column: column.to_string(),
|
||||
/// };
|
||||
/// let definition = PropertyGraphDefinition::MaterializedView {
|
||||
/// nodes: vec![NodeTable {
|
||||
/// table: "person".to_string(),
|
||||
/// key: "person_id".to_string(),
|
||||
/// label: "Person".to_string(),
|
||||
/// properties: None,
|
||||
/// }],
|
||||
/// edges: vec![EdgeTable {
|
||||
/// table: "knows".to_string(),
|
||||
/// label: "KNOWS".to_string(),
|
||||
/// source: Endpoint { column: "src_id".to_string(), references: person.clone() },
|
||||
/// destination: Endpoint { column: "dst_id".to_string(), references: person },
|
||||
/// properties: None,
|
||||
/// }],
|
||||
/// };
|
||||
/// let graph = connection.create_property_graph("social", &definition, &[]).await?;
|
||||
/// println!("{} nodes, {} edges", graph.vertex_count, graph.edge_count);
|
||||
/// let knows = EdgeType::new("KNOWS", endpoint("src_id"), endpoint("dst_id"), None)?;
|
||||
/// let definition = PropertyGraphDefinition { nodes: vec![person], edges: vec![knows] };
|
||||
/// connection.create_property_graph("people", &definition, &[]).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -916,10 +905,9 @@ impl Connection {
|
||||
.await
|
||||
}
|
||||
|
||||
/// What this database records about one property graph: its mode and
|
||||
/// definition, its current and previous commits, its size, and, for a
|
||||
/// materialized view, the table versions it was refreshed from. Local
|
||||
/// databases return [`Error::NotSupported`].
|
||||
/// What this database records about one property graph: its node and edge
|
||||
/// types, its current and previous commits, and its size. Local databases
|
||||
/// return [`Error::NotSupported`].
|
||||
pub async fn describe_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
@@ -931,10 +919,8 @@ impl Connection {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Drop a property graph and wait for its data to be deleted.
|
||||
///
|
||||
/// The tables it reads are untouched. Local databases return
|
||||
/// [`Error::NotSupported`].
|
||||
/// Drop a property graph and wait for its data to be deleted. Local
|
||||
/// databases return [`Error::NotSupported`].
|
||||
pub async fn drop_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
@@ -955,15 +941,13 @@ impl Connection {
|
||||
self.internal.list_property_graphs(namespace_path).await
|
||||
}
|
||||
|
||||
/// Insert rows into an independent property graph, as one commit.
|
||||
/// Insert rows into a property graph, as one commit.
|
||||
///
|
||||
/// The rows are one node label's or one edge label's. Nodes upsert by key:
|
||||
/// a key the graph holds replaces that node's properties. Edges append,
|
||||
/// carrying their endpoints' keys in the columns the edge type names; an
|
||||
/// edge whose endpoint is not a node of the graph refuses the whole
|
||||
/// insert. A materialized view graph is written through its tables and
|
||||
/// [refreshed](Self::refresh_property_graph) instead. Local databases
|
||||
/// return [`Error::NotSupported`].
|
||||
/// insert. Local databases return [`Error::NotSupported`].
|
||||
pub async fn insert_into_property_graph<T: Scannable + 'static>(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
@@ -982,23 +966,6 @@ impl Connection {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Bring a materialized view property graph up to its tables' latest
|
||||
/// versions.
|
||||
///
|
||||
/// Until it is refreshed, a query over a table that has changed reads the
|
||||
/// tables rather than the graph. Local databases return
|
||||
/// [`Error::NotSupported`].
|
||||
pub async fn refresh_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.refresh_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Return a property graph to its previous commit.
|
||||
///
|
||||
/// The commit rolled back from is discarded, so a second rollback in a row
|
||||
@@ -1014,6 +981,166 @@ impl Connection {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a virtual property graph over tables in its namespace.
|
||||
///
|
||||
/// The definition names the tables that hold its nodes, with their keys
|
||||
/// and labels, and its edges, with the node keys their source and
|
||||
/// destination columns reference. The graph stores nothing else: a query
|
||||
/// reads the tables as they are then. A name held by a graph of any kind
|
||||
/// is taken. Local databases return [`Error::NotSupported`].
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use lancedb::graph::{EdgeTable, Endpoint, EndpointReference, NodeTable,
|
||||
/// # VirtualPropertyGraphDefinition};
|
||||
/// # async fn example(connection: &lancedb::Connection) -> lancedb::Result<()> {
|
||||
/// let person = EndpointReference {
|
||||
/// table: "person".to_string(),
|
||||
/// column: "person_id".to_string(),
|
||||
/// };
|
||||
/// let definition = VirtualPropertyGraphDefinition {
|
||||
/// nodes: vec![NodeTable {
|
||||
/// table: "person".to_string(),
|
||||
/// key: "person_id".to_string(),
|
||||
/// label: "Person".to_string(),
|
||||
/// properties: None,
|
||||
/// }],
|
||||
/// edges: vec![EdgeTable {
|
||||
/// table: "knows".to_string(),
|
||||
/// label: "KNOWS".to_string(),
|
||||
/// source: Endpoint { column: "src_id".to_string(), references: person.clone() },
|
||||
/// destination: Endpoint { column: "dst_id".to_string(), references: person },
|
||||
/// properties: None,
|
||||
/// }],
|
||||
/// };
|
||||
/// connection.create_virtual_property_graph("social", &definition, &[]).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn create_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
definition: &VirtualPropertyGraphDefinition,
|
||||
namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.create_virtual_property_graph(name.as_ref(), definition, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// What this database records about one virtual property graph: its node
|
||||
/// and edge tables. Local databases return [`Error::NotSupported`].
|
||||
pub async fn describe_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.describe_virtual_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Drop a virtual property graph and wait for its definition to be
|
||||
/// deleted. The tables it reads are untouched. Local databases return
|
||||
/// [`Error::NotSupported`].
|
||||
pub async fn drop_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.drop_virtual_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// The names of the virtual property graphs in one namespace. Local
|
||||
/// databases return [`Error::NotSupported`].
|
||||
pub async fn list_virtual_property_graphs(
|
||||
&self,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
validate_namespace(namespace_path)?;
|
||||
self.internal
|
||||
.list_virtual_property_graphs(namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a materialized virtual property graph: a virtual property
|
||||
/// graph's definition, built now from the tables' latest versions and
|
||||
/// read as that build until
|
||||
/// [refreshed](Self::refresh_materialized_virtual_property_graph). Local
|
||||
/// databases return [`Error::NotSupported`].
|
||||
pub async fn create_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
definition: &VirtualPropertyGraphDefinition,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.create_materialized_virtual_property_graph(name.as_ref(), definition, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// What this database records about one materialized virtual property
|
||||
/// graph: its node and edge tables, and what its last refresh built from
|
||||
/// them. Local databases return [`Error::NotSupported`].
|
||||
pub async fn describe_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.describe_materialized_virtual_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Drop a materialized virtual property graph and wait for its data to be
|
||||
/// deleted. The tables it reads are untouched. Local databases return
|
||||
/// [`Error::NotSupported`].
|
||||
pub async fn drop_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.drop_materialized_virtual_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// The names of the materialized virtual property graphs in one
|
||||
/// namespace. Local databases return [`Error::NotSupported`].
|
||||
pub async fn list_materialized_virtual_property_graphs(
|
||||
&self,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
validate_namespace(namespace_path)?;
|
||||
self.internal
|
||||
.list_materialized_virtual_property_graphs(namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rebuild a materialized virtual property graph from its tables' latest
|
||||
/// versions.
|
||||
///
|
||||
/// When every table's commits since the last refresh left its rows as
|
||||
/// they were, only the recorded versions advance. Local databases return
|
||||
/// [`Error::NotSupported`].
|
||||
pub async fn refresh_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
validate_property_graph_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.refresh_materialized_virtual_property_graph(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rename a table in the database.
|
||||
///
|
||||
/// This is only supported in LanceDB Cloud.
|
||||
|
||||
+105
-14
@@ -28,7 +28,10 @@ use lance_namespace::models::{
|
||||
|
||||
use crate::data::scannable::Scannable;
|
||||
use crate::error::Result;
|
||||
use crate::graph::{PropertyGraphDefinition, PropertyGraphDescription};
|
||||
use crate::graph::{
|
||||
MaterializedVirtualPropertyGraphDescription, PropertyGraphDefinition, PropertyGraphDescription,
|
||||
VirtualPropertyGraphDefinition, VirtualPropertyGraphDescription,
|
||||
};
|
||||
use crate::job::Job;
|
||||
use crate::materialized_view::CreateMaterializedViewRequest;
|
||||
use crate::secrets::SecretInfo;
|
||||
@@ -514,7 +517,7 @@ pub trait Database:
|
||||
async fn list_views(&self, _namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
view_ops_not_supported()
|
||||
}
|
||||
/// Create a property graph over tables in its namespace.
|
||||
/// Create a property graph, empty.
|
||||
async fn create_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
@@ -531,8 +534,7 @@ pub trait Database:
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Drop a property graph and wait for its data to be deleted. The tables it
|
||||
/// reads are untouched.
|
||||
/// Drop a property graph and wait for its data to be deleted.
|
||||
async fn drop_property_graph(&self, _name: &str, _namespace_path: &[String]) -> Result<()> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
@@ -550,8 +552,8 @@ pub trait Database:
|
||||
async fn list_property_graphs(&self, _namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Insert rows of one node label or edge label into an independent
|
||||
/// property graph, as one commit.
|
||||
/// Insert rows of one node label or edge label into a property graph, as
|
||||
/// one commit.
|
||||
async fn insert_into_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
@@ -561,14 +563,6 @@ pub trait Database:
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Bring a materialized view property graph up to its tables.
|
||||
async fn refresh_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Make a property graph's previous commit current again.
|
||||
async fn rollback_property_graph(
|
||||
&self,
|
||||
@@ -577,6 +571,103 @@ pub trait Database:
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Create a virtual property graph over tables in its namespace.
|
||||
async fn create_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_definition: &VirtualPropertyGraphDefinition,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// What the database records about one virtual property graph.
|
||||
async fn describe_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Drop a virtual property graph and wait for its definition to be
|
||||
/// deleted. The tables it reads are untouched.
|
||||
async fn drop_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Drop a virtual property graph and return the job deleting its
|
||||
/// definition, without waiting.
|
||||
#[doc(hidden)]
|
||||
async fn drop_virtual_property_graph_async(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// The names of the virtual property graphs in one namespace.
|
||||
async fn list_virtual_property_graphs(
|
||||
&self,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Create a materialized virtual property graph over tables in its
|
||||
/// namespace, built from them now.
|
||||
async fn create_materialized_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_definition: &VirtualPropertyGraphDefinition,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// What the database records about one materialized virtual property
|
||||
/// graph.
|
||||
async fn describe_materialized_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Drop a materialized virtual property graph and wait for its data to be
|
||||
/// deleted. The tables it reads are untouched.
|
||||
async fn drop_materialized_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Drop a materialized virtual property graph and return the job deleting
|
||||
/// its data, without waiting.
|
||||
#[doc(hidden)]
|
||||
async fn drop_materialized_virtual_property_graph_async(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// The names of the materialized virtual property graphs in one namespace.
|
||||
async fn list_materialized_virtual_property_graphs(
|
||||
&self,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Rebuild a materialized virtual property graph from its tables' latest
|
||||
/// versions.
|
||||
async fn refresh_materialized_virtual_property_graph(
|
||||
&self,
|
||||
_name: &str,
|
||||
_namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
property_graph_ops_not_supported()
|
||||
}
|
||||
/// Open a job by id, returning a handle with its record already
|
||||
/// populated. Fails with [`crate::Error::JobNotFound`] when the server has
|
||||
/// no such job.
|
||||
|
||||
+160
-54
@@ -1,15 +1,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
//! Property graphs: catalog objects read as graphs, in one of two modes.
|
||||
//! Property graphs: catalog objects read as graphs, in three kinds that share
|
||||
//! one namespace of names.
|
||||
//!
|
||||
//! - Independent: the graph holds its own rows. Its node and edge types have
|
||||
//! - A property graph holds its own rows. Its node and edge types have
|
||||
//! schemas, and rows are inserted into the graph itself.
|
||||
//! - Materialized view: the graph reads tables in its namespace -- which hold
|
||||
//! - A virtual property graph reads tables in its namespace -- which hold
|
||||
//! nodes and which hold edges, each node table's key and label, each edge
|
||||
//! table's label and how its source and destination columns reference node
|
||||
//! keys, the shape of SQL/PGQ's `CREATE PROPERTY GRAPH` -- and is refreshed
|
||||
//! from them. The tables stay ordinary tables.
|
||||
//! keys, the shape of SQL/PGQ's `CREATE PROPERTY GRAPH` -- as they are when
|
||||
//! a query reads them.
|
||||
//! - A materialized virtual property graph has a virtual graph's definition,
|
||||
//! and is read as its last refresh from the tables built it.
|
||||
//!
|
||||
//! The verbs live on [`crate::connection::Connection`].
|
||||
|
||||
@@ -19,38 +22,54 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// A graph's definition, tagged with its mode.
|
||||
fn from_json<T: serde::de::DeserializeOwned>(json: &str) -> Result<T> {
|
||||
serde_json::from_str(json).map_err(|source| Error::InvalidInput {
|
||||
message: format!("invalid property graph definition: {source}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_json(value: &impl Serialize) -> Result<String> {
|
||||
serde_json::to_string(value).map_err(|source| Error::Runtime {
|
||||
message: format!("could not encode a property graph description: {source}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// A property graph's node and edge types.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "mode", rename_all = "snake_case")]
|
||||
pub enum PropertyGraphDefinition {
|
||||
/// The graph holds its own rows, inserted into it.
|
||||
Independent {
|
||||
/// The node types, one per label.
|
||||
nodes: Vec<NodeType>,
|
||||
/// The edge types, one per label.
|
||||
#[serde(default)]
|
||||
edges: Vec<EdgeType>,
|
||||
},
|
||||
/// The graph reads tables and is refreshed from them.
|
||||
MaterializedView {
|
||||
/// The node tables, one per label.
|
||||
nodes: Vec<NodeTable>,
|
||||
/// The edge tables, one per label.
|
||||
#[serde(default)]
|
||||
edges: Vec<EdgeTable>,
|
||||
},
|
||||
pub struct PropertyGraphDefinition {
|
||||
/// The node types, one per label.
|
||||
pub nodes: Vec<NodeType>,
|
||||
/// The edge types, one per label.
|
||||
#[serde(default)]
|
||||
pub edges: Vec<EdgeType>,
|
||||
}
|
||||
|
||||
impl PropertyGraphDefinition {
|
||||
/// Parse a definition from its JSON form.
|
||||
pub fn from_json(json: &str) -> Result<Self> {
|
||||
serde_json::from_str(json).map_err(|source| Error::InvalidInput {
|
||||
message: format!("invalid property graph definition: {source}"),
|
||||
})
|
||||
from_json(json)
|
||||
}
|
||||
}
|
||||
|
||||
/// The nodes of one label of an independent graph.
|
||||
/// The node and edge tables of a virtual property graph, which a materialized
|
||||
/// virtual property graph shares.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VirtualPropertyGraphDefinition {
|
||||
/// The node tables, one per label.
|
||||
pub nodes: Vec<NodeTable>,
|
||||
/// The edge tables, one per label.
|
||||
#[serde(default)]
|
||||
pub edges: Vec<EdgeTable>,
|
||||
}
|
||||
|
||||
impl VirtualPropertyGraphDefinition {
|
||||
/// Parse a definition from its JSON form.
|
||||
pub fn from_json(json: &str) -> Result<Self> {
|
||||
from_json(json)
|
||||
}
|
||||
}
|
||||
|
||||
/// The nodes of one label of a property graph.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NodeType {
|
||||
/// The label its nodes carry.
|
||||
@@ -79,7 +98,7 @@ impl NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
/// The edges of one label of an independent graph.
|
||||
/// The edges of one label of a property graph.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EdgeType {
|
||||
/// The label its edges carry.
|
||||
@@ -180,7 +199,7 @@ pub struct EndpointReference {
|
||||
pub column: String,
|
||||
}
|
||||
|
||||
/// The version of one table a graph's current commit was refreshed from.
|
||||
/// The version of one table a materialized graph's last refresh read.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GraphSourceVersion {
|
||||
/// The table.
|
||||
@@ -192,7 +211,7 @@ pub struct GraphSourceVersion {
|
||||
/// What a database records about one property graph.
|
||||
///
|
||||
/// Returned by [`crate::connection::Connection::describe_property_graph`], and
|
||||
/// by every call that writes a graph.
|
||||
/// by every call that writes one.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PropertyGraphDescription {
|
||||
/// The graph's name within its namespace.
|
||||
@@ -200,7 +219,7 @@ pub struct PropertyGraphDescription {
|
||||
/// The namespace holding the graph; empty is the root namespace.
|
||||
#[serde(rename = "namespace", default)]
|
||||
pub namespace_path: Vec<String>,
|
||||
/// The graph's mode and definition.
|
||||
/// The graph's node and edge types.
|
||||
#[serde(flatten)]
|
||||
pub definition: PropertyGraphDefinition,
|
||||
/// The graph's current commit.
|
||||
@@ -214,18 +233,65 @@ pub struct PropertyGraphDescription {
|
||||
pub vertex_count: u64,
|
||||
/// How many edges the current commit holds.
|
||||
pub edge_count: u64,
|
||||
/// The table versions a materialized view's current commit was refreshed
|
||||
/// from; empty for an independent graph.
|
||||
#[serde(default)]
|
||||
pub sources: Vec<GraphSourceVersion>,
|
||||
}
|
||||
|
||||
impl PropertyGraphDescription {
|
||||
/// The description in its JSON form.
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
serde_json::to_string(self).map_err(|source| Error::Runtime {
|
||||
message: format!("could not encode a property graph description: {source}"),
|
||||
})
|
||||
to_json(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a database records about one virtual property graph: its node and
|
||||
/// edge tables, with each table's properties as create resolved them.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VirtualPropertyGraphDescription {
|
||||
/// The graph's name within its namespace.
|
||||
pub name: String,
|
||||
/// The namespace holding the graph; empty is the root namespace.
|
||||
#[serde(rename = "namespace", default)]
|
||||
pub namespace_path: Vec<String>,
|
||||
/// The graph's node and edge tables.
|
||||
#[serde(flatten)]
|
||||
pub definition: VirtualPropertyGraphDefinition,
|
||||
}
|
||||
|
||||
impl VirtualPropertyGraphDescription {
|
||||
/// The description in its JSON form.
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
to_json(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a database records about one materialized virtual property graph:
|
||||
/// its node and edge tables, and what its last refresh built from them.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MaterializedVirtualPropertyGraphDescription {
|
||||
/// The graph's name within its namespace.
|
||||
pub name: String,
|
||||
/// The namespace holding the graph; empty is the root namespace.
|
||||
#[serde(rename = "namespace", default)]
|
||||
pub namespace_path: Vec<String>,
|
||||
/// The graph's node and edge tables.
|
||||
#[serde(flatten)]
|
||||
pub definition: VirtualPropertyGraphDefinition,
|
||||
/// The last refresh's commit.
|
||||
pub commit: String,
|
||||
/// When the last refresh landed, as RFC 3339.
|
||||
pub committed_at: String,
|
||||
/// How many nodes the last refresh built.
|
||||
pub vertex_count: u64,
|
||||
/// How many edges the last refresh built.
|
||||
pub edge_count: u64,
|
||||
/// The version of each table the last refresh read.
|
||||
#[serde(default)]
|
||||
pub sources: Vec<GraphSourceVersion>,
|
||||
}
|
||||
|
||||
impl MaterializedVirtualPropertyGraphDescription {
|
||||
/// The description in its JSON form.
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
to_json(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,12 +301,12 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn social() -> PropertyGraphDefinition {
|
||||
fn social() -> VirtualPropertyGraphDefinition {
|
||||
let person = EndpointReference {
|
||||
table: "person".to_string(),
|
||||
column: "person_id".to_string(),
|
||||
};
|
||||
PropertyGraphDefinition::MaterializedView {
|
||||
VirtualPropertyGraphDefinition {
|
||||
nodes: vec![NodeTable {
|
||||
table: "person".to_string(),
|
||||
key: "person_id".to_string(),
|
||||
@@ -264,12 +330,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_json_is_the_sql_pgq_shape() {
|
||||
fn test_a_virtual_definition_is_the_sql_pgq_shape() {
|
||||
let json = serde_json::to_value(social()).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
serde_json::json!({
|
||||
"mode": "materialized_view",
|
||||
"nodes": [{"table": "person", "key": "person_id", "label": "Person",
|
||||
"properties": ["name"]}],
|
||||
"edges": [{"table": "knows", "label": "KNOWS",
|
||||
@@ -281,14 +346,14 @@ mod tests {
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
PropertyGraphDefinition::from_json(&json.to_string()).unwrap(),
|
||||
VirtualPropertyGraphDefinition::from_json(&json.to_string()).unwrap(),
|
||||
social()
|
||||
);
|
||||
assert!(PropertyGraphDefinition::from_json(r#"{"nodes": []}"#).is_err());
|
||||
assert!(VirtualPropertyGraphDefinition::from_json(r#"{"edges": []}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_an_independent_definition_carries_its_schemas() {
|
||||
fn test_a_property_graph_definition_carries_its_schemas() {
|
||||
let person = NodeType::new(
|
||||
"Person",
|
||||
"person_id",
|
||||
@@ -311,12 +376,12 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let definition = PropertyGraphDefinition::Independent {
|
||||
let definition = PropertyGraphDefinition {
|
||||
nodes: vec![person.clone()],
|
||||
edges: vec![knows],
|
||||
};
|
||||
let json = serde_json::to_value(&definition).unwrap();
|
||||
assert_eq!(json["mode"], "independent");
|
||||
assert!(json.get("mode").is_none());
|
||||
assert_eq!(json["nodes"][0]["schema"]["fields"][0]["name"], "person_id");
|
||||
assert!(json["edges"][0].get("schema").is_none());
|
||||
assert_eq!(
|
||||
@@ -329,6 +394,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_materialized_description_carries_its_definition_at_the_top_level() {
|
||||
let description = MaterializedVirtualPropertyGraphDescription {
|
||||
name: "social".to_string(),
|
||||
namespace_path: vec!["analytics".to_string()],
|
||||
definition: social(),
|
||||
commit: "c1".to_string(),
|
||||
committed_at: "2026-09-26T17:00:00Z".to_string(),
|
||||
vertex_count: 4,
|
||||
edge_count: 5,
|
||||
sources: vec![GraphSourceVersion {
|
||||
table: "person".to_string(),
|
||||
version: 3,
|
||||
}],
|
||||
};
|
||||
let json = serde_json::to_value(&description).unwrap();
|
||||
assert_eq!(json["namespace"], serde_json::json!(["analytics"]));
|
||||
assert_eq!(json["nodes"][0]["table"], "person");
|
||||
let decoded: MaterializedVirtualPropertyGraphDescription =
|
||||
serde_json::from_value(json).unwrap();
|
||||
assert_eq!(decoded, description);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_local_databases_refuse_property_graphs() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -343,27 +431,45 @@ mod tests {
|
||||
"{error}"
|
||||
);
|
||||
};
|
||||
let people = PropertyGraphDefinition {
|
||||
nodes: vec![],
|
||||
edges: vec![],
|
||||
};
|
||||
refused(
|
||||
conn.create_property_graph("social", &social(), &[])
|
||||
conn.create_property_graph("people", &people, &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(
|
||||
conn.describe_property_graph("social", &[])
|
||||
conn.create_virtual_property_graph("social", &social(), &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(
|
||||
conn.refresh_property_graph("social", &[])
|
||||
conn.create_materialized_virtual_property_graph("social", &social(), &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(
|
||||
conn.rollback_property_graph("social", &[])
|
||||
conn.describe_virtual_property_graph("social", &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(
|
||||
conn.refresh_materialized_virtual_property_graph("social", &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(
|
||||
conn.rollback_property_graph("people", &[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(conn.drop_property_graph("people", &[]).await.unwrap_err());
|
||||
refused(
|
||||
conn.list_materialized_virtual_property_graphs(&[])
|
||||
.await
|
||||
.unwrap_err(),
|
||||
);
|
||||
refused(conn.drop_property_graph("social", &[]).await.unwrap_err());
|
||||
refused(conn.list_property_graphs(&[]).await.unwrap_err());
|
||||
}
|
||||
}
|
||||
|
||||
+444
-197
@@ -29,7 +29,10 @@ use crate::function::{
|
||||
FunctionArtifactRequest, FunctionRegistrationRequest, FunctionSignature, FunctionVersion,
|
||||
PythonRuntimeSpec,
|
||||
};
|
||||
use crate::graph::{PropertyGraphDefinition, PropertyGraphDescription};
|
||||
use crate::graph::{
|
||||
MaterializedVirtualPropertyGraphDescription, PropertyGraphDefinition, PropertyGraphDescription,
|
||||
VirtualPropertyGraphDefinition, VirtualPropertyGraphDescription,
|
||||
};
|
||||
use crate::job::Job;
|
||||
use crate::materialized_view::CreateMaterializedViewRequest;
|
||||
use crate::remote::job::{PauseJobResponse, RemoteJob, ResumeJobResponse, job_state_to_client};
|
||||
@@ -841,12 +844,119 @@ struct RemoteListViewsResponse {
|
||||
page_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RemoteListPropertyGraphsResponse {
|
||||
#[serde(default)]
|
||||
property_graphs: Vec<String>,
|
||||
#[serde(default)]
|
||||
page_token: Option<String>,
|
||||
/// The route prefix of each kind of graph.
|
||||
const PROPERTY_GRAPH: &str = "property_graph";
|
||||
const VIRTUAL_PROPERTY_GRAPH: &str = "virtual_property_graph";
|
||||
const MATERIALIZED_VIRTUAL_PROPERTY_GRAPH: &str = "materialized_virtual_property_graph";
|
||||
|
||||
/// A graph definition as a create request's body.
|
||||
fn graph_body(definition: &impl serde::Serialize) -> Result<serde_json::Value> {
|
||||
serde_json::to_value(definition).map_err(|source| Error::InvalidInput {
|
||||
message: format!("could not encode a property graph definition: {source}"),
|
||||
})
|
||||
}
|
||||
|
||||
impl<S: HttpSend> RemoteDatabase<S> {
|
||||
/// POST one route of a graph of `kind` and decode what it answers.
|
||||
async fn graph_request<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
verb: &str,
|
||||
body: Option<serde_json::Value>,
|
||||
) -> Result<T> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let mut req = self.client.post(&format!("/v1/{kind}/{graph_id}/{verb}"));
|
||||
if let Some(body) = body {
|
||||
req = req.json(&body);
|
||||
}
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
response.json().await.err_to_http(request_id)
|
||||
}
|
||||
|
||||
/// Drop a graph of `kind` and return the job deleting its storage.
|
||||
async fn drop_graph_async(
|
||||
&self,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self.client.post(&format!("/v1/{kind}/{graph_id}/drop"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.err_to_http(request_id.clone())?;
|
||||
match status {
|
||||
// Nothing was bound to the name, so nothing is being deleted.
|
||||
StatusCode::OK => Ok(Job::new_done()),
|
||||
StatusCode::ACCEPTED => {
|
||||
let job_id = extract_job_id(&body).ok_or_else(|| Error::Http {
|
||||
source: "property graph drop response did not contain a valid job_id".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
})?;
|
||||
Ok(Job::new(Box::new(RemoteJob::new(
|
||||
self.client.clone(),
|
||||
job_id,
|
||||
))))
|
||||
}
|
||||
_ => Err(Error::Http {
|
||||
source: "property graph drop must return 200 OK or 202 Accepted".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The names of the graphs of `kind` in one namespace, every page of them.
|
||||
async fn list_graphs(&self, kind: &str, namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
let namespace_id = build_namespace_identifier(namespace_path)?;
|
||||
let path = format!("/v1/namespace/{namespace_id}/{kind}/list");
|
||||
// A listing names its field after the kind: `property_graphs`, ...
|
||||
let field = format!("{kind}s");
|
||||
let mut graphs = Vec::new();
|
||||
let mut page_token: Option<String> = None;
|
||||
let mut seen_page_tokens = HashSet::new();
|
||||
loop {
|
||||
let mut req = self.client.get(&path);
|
||||
if let Some(token) = &page_token {
|
||||
req = req.query(&[("page_token", token)]);
|
||||
}
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
let status = response.status();
|
||||
let mut page: serde_json::Map<String, serde_json::Value> =
|
||||
response.json().await.err_to_http(request_id.clone())?;
|
||||
let names: Vec<String> = match page.remove(&field) {
|
||||
Some(names) => serde_json::from_value(names).map_err(|source| Error::Http {
|
||||
source: format!("{kind} listing response is malformed: {source}").into(),
|
||||
request_id: request_id.clone(),
|
||||
status_code: Some(status),
|
||||
})?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
graphs.extend(names);
|
||||
let Some(next_page_token) = page
|
||||
.remove("page_token")
|
||||
.and_then(|token| token.as_str().map(str::to_string))
|
||||
.filter(|token| !token.is_empty())
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if !seen_page_tokens.insert(next_page_token.clone()) {
|
||||
return Err(Error::Http {
|
||||
source: "Property graph listing response repeated a page_token".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
});
|
||||
}
|
||||
page_token = Some(next_page_token);
|
||||
}
|
||||
Ok(graphs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bound on `list_jobs` page walking; a warning is logged when the listing
|
||||
@@ -1294,14 +1404,9 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
definition: &PropertyGraphDefinition,
|
||||
namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/create"))
|
||||
.json(definition);
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
response.json().await.err_to_http(request_id)
|
||||
let body = graph_body(definition)?;
|
||||
self.graph_request(PROPERTY_GRAPH, name, namespace_path, "create", Some(body))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn describe_property_graph(
|
||||
@@ -1309,13 +1414,8 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/describe"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
response.json().await.err_to_http(request_id)
|
||||
self.graph_request(PROPERTY_GRAPH, name, namespace_path, "describe", None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_property_graph(&self, name: &str, namespace_path: &[String]) -> Result<()> {
|
||||
@@ -1330,34 +1430,12 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/drop"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.err_to_http(request_id.clone())?;
|
||||
match status {
|
||||
// Nothing was bound to the name, so nothing is being deleted.
|
||||
StatusCode::OK => Ok(Job::new_done()),
|
||||
StatusCode::ACCEPTED => {
|
||||
let job_id = extract_job_id(&body).ok_or_else(|| Error::Http {
|
||||
source: "property graph drop response did not contain a valid job_id".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
})?;
|
||||
Ok(Job::new(Box::new(RemoteJob::new(
|
||||
self.client.clone(),
|
||||
job_id,
|
||||
))))
|
||||
}
|
||||
_ => Err(Error::Http {
|
||||
source: "property graph drop must return 200 OK or 202 Accepted".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
}),
|
||||
}
|
||||
self.drop_graph_async(PROPERTY_GRAPH, name, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_property_graphs(&self, namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
self.list_graphs(PROPERTY_GRAPH, namespace_path).await
|
||||
}
|
||||
|
||||
async fn insert_into_property_graph(
|
||||
@@ -1381,7 +1459,7 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
}
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/insert"))
|
||||
.post(&format!("/v1/{PROPERTY_GRAPH}/{graph_id}/insert"))
|
||||
.query(&[("label", label)])
|
||||
.body(body)
|
||||
.header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE);
|
||||
@@ -1390,65 +1468,145 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
response.json().await.err_to_http(request_id)
|
||||
}
|
||||
|
||||
async fn refresh_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/refresh"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
response.json().await.err_to_http(request_id)
|
||||
}
|
||||
|
||||
async fn rollback_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<PropertyGraphDescription> {
|
||||
let graph_id = build_object_identifier("Property graph name", name, namespace_path)?;
|
||||
let req = self
|
||||
.client
|
||||
.post(&format!("/v1/property_graph/{graph_id}/rollback"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
response.json().await.err_to_http(request_id)
|
||||
self.graph_request(PROPERTY_GRAPH, name, namespace_path, "rollback", None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_property_graphs(&self, namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
let namespace_id = build_namespace_identifier(namespace_path)?;
|
||||
let path = format!("/v1/namespace/{namespace_id}/property_graph/list");
|
||||
let mut graphs = Vec::new();
|
||||
let mut page_token: Option<String> = None;
|
||||
let mut seen_page_tokens = HashSet::new();
|
||||
loop {
|
||||
let mut req = self.client.get(&path);
|
||||
if let Some(token) = &page_token {
|
||||
req = req.query(&[("page_token", token)]);
|
||||
}
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
let status = response.status();
|
||||
let response: RemoteListPropertyGraphsResponse =
|
||||
response.json().await.err_to_http(request_id.clone())?;
|
||||
graphs.extend(response.property_graphs);
|
||||
let Some(next_page_token) = response.page_token.filter(|token| !token.is_empty())
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if !seen_page_tokens.insert(next_page_token.clone()) {
|
||||
return Err(Error::Http {
|
||||
source: "Property graph listing response repeated a page_token".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
});
|
||||
}
|
||||
page_token = Some(next_page_token);
|
||||
}
|
||||
Ok(graphs)
|
||||
async fn create_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
definition: &VirtualPropertyGraphDefinition,
|
||||
namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
let body = graph_body(definition)?;
|
||||
self.graph_request(
|
||||
VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
namespace_path,
|
||||
"create",
|
||||
Some(body),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn describe_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<VirtualPropertyGraphDescription> {
|
||||
self.graph_request(
|
||||
VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
namespace_path,
|
||||
"describe",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
self.drop_virtual_property_graph_async(name, namespace_path)
|
||||
.await?
|
||||
.wait()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_virtual_property_graph_async(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
self.drop_graph_async(VIRTUAL_PROPERTY_GRAPH, name, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_virtual_property_graphs(&self, namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
self.list_graphs(VIRTUAL_PROPERTY_GRAPH, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
definition: &VirtualPropertyGraphDefinition,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
let body = graph_body(definition)?;
|
||||
self.graph_request(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
namespace_path,
|
||||
"create",
|
||||
Some(body),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn describe_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
self.graph_request(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
namespace_path,
|
||||
"describe",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<()> {
|
||||
self.drop_materialized_virtual_property_graph_async(name, namespace_path)
|
||||
.await?
|
||||
.wait()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_materialized_virtual_property_graph_async(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Job> {
|
||||
self.drop_graph_async(MATERIALIZED_VIRTUAL_PROPERTY_GRAPH, name, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_materialized_virtual_property_graphs(
|
||||
&self,
|
||||
namespace_path: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
self.list_graphs(MATERIALIZED_VIRTUAL_PROPERTY_GRAPH, namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn refresh_materialized_virtual_property_graph(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace_path: &[String],
|
||||
) -> Result<MaterializedVirtualPropertyGraphDescription> {
|
||||
self.graph_request(
|
||||
MATERIALIZED_VIRTUAL_PROPERTY_GRAPH,
|
||||
name,
|
||||
namespace_path,
|
||||
"refresh",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn open_job(&self, job_id: &str) -> Result<Job> {
|
||||
@@ -4453,10 +4611,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn social_graph() -> crate::graph::PropertyGraphDefinition {
|
||||
crate::graph::PropertyGraphDefinition::from_json(
|
||||
r#"{"mode": "materialized_view",
|
||||
"nodes": [{"table": "person", "key": "person_id", "label": "Person"}],
|
||||
fn social_graph() -> crate::graph::VirtualPropertyGraphDefinition {
|
||||
crate::graph::VirtualPropertyGraphDefinition::from_json(
|
||||
r#"{"nodes": [{"table": "person", "key": "person_id", "label": "Person"}],
|
||||
"edges": [{"table": "knows", "label": "KNOWS",
|
||||
"source": {"column": "src_id",
|
||||
"references": {"table": "person", "column": "person_id"}},
|
||||
@@ -4468,50 +4625,75 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn property_graph_description_body(name: &str, namespace: &[&str]) -> String {
|
||||
let mut body = serde_json::to_value(social_graph()).unwrap();
|
||||
fn people_graph() -> crate::graph::PropertyGraphDefinition {
|
||||
crate::graph::PropertyGraphDefinition::from_json(
|
||||
r#"{"nodes": [{"label": "Person", "key": "person_id", "schema": {"fields": [
|
||||
{"name": "person_id", "type": {"type": "int64"}, "nullable": false}]}}],
|
||||
"edges": [{"label": "KNOWS",
|
||||
"source": {"label": "Person", "column": "src_id"},
|
||||
"destination": {"label": "Person", "column": "dst_id"}}]}"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// A description of `definition`, with `extra` fields beside it.
|
||||
fn graph_description_body(
|
||||
definition: serde_json::Value,
|
||||
name: &str,
|
||||
namespace: &[&str],
|
||||
extra: serde_json::Value,
|
||||
) -> String {
|
||||
let mut body = definition;
|
||||
let fields = body.as_object_mut().unwrap();
|
||||
fields.insert("name".into(), name.into());
|
||||
fields.insert("namespace".into(), namespace.into());
|
||||
fields.insert(
|
||||
"commit".into(),
|
||||
"6f1c2d3e-0000-4000-8000-000000000000".into(),
|
||||
);
|
||||
fields.insert("committed_at".into(), "2026-09-26T17:00:00Z".into());
|
||||
fields.insert("vertex_count".into(), 4.into());
|
||||
fields.insert("edge_count".into(), 5.into());
|
||||
fields.insert(
|
||||
"sources".into(),
|
||||
serde_json::json!([{"table": "person", "version": 3}, {"table": "knows", "version": 2}]),
|
||||
);
|
||||
fields.extend(extra.as_object().unwrap().clone());
|
||||
body.to_string()
|
||||
}
|
||||
|
||||
fn materialized_body(name: &str, namespace: &[&str]) -> String {
|
||||
graph_description_body(
|
||||
serde_json::to_value(social_graph()).unwrap(),
|
||||
name,
|
||||
namespace,
|
||||
serde_json::json!({
|
||||
"commit": "6f1c2d3e-0000-4000-8000-000000000000",
|
||||
"committed_at": "2026-09-26T17:00:00Z",
|
||||
"vertex_count": 4,
|
||||
"edge_count": 5,
|
||||
"sources": [{"table": "person", "version": 3}, {"table": "knows", "version": 2}],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_property_graph_posts_the_definition() {
|
||||
async fn test_create_materialized_virtual_property_graph_posts_the_definition() {
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::POST);
|
||||
assert_eq!(
|
||||
request.url().path(),
|
||||
"/v1/property_graph/analytics$social/create"
|
||||
"/v1/materialized_virtual_property_graph/analytics$social/create"
|
||||
);
|
||||
let body: serde_json::Value =
|
||||
serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap();
|
||||
assert_eq!(body, serde_json::to_value(social_graph()).unwrap());
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(property_graph_description_body("social", &["analytics"]))
|
||||
.body(materialized_body("social", &["analytics"]))
|
||||
.unwrap()
|
||||
});
|
||||
let graph = conn
|
||||
.create_property_graph("social", &social_graph(), &["analytics".into()])
|
||||
.create_materialized_virtual_property_graph(
|
||||
"social",
|
||||
&social_graph(),
|
||||
&["analytics".into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(graph.name, "social");
|
||||
assert_eq!(graph.namespace_path, vec!["analytics".to_string()]);
|
||||
assert_eq!(graph.definition, social_graph());
|
||||
assert_eq!(graph.commit, "6f1c2d3e-0000-4000-8000-000000000000");
|
||||
assert_eq!(graph.previous_commit, None);
|
||||
assert_eq!((graph.vertex_count, graph.edge_count), (4, 5));
|
||||
assert_eq!(
|
||||
graph.sources[1],
|
||||
@@ -4523,29 +4705,97 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_graph_writes_post_to_their_routes() {
|
||||
async fn test_each_kind_of_graph_has_its_own_routes() {
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::POST);
|
||||
match request.url().path() {
|
||||
"/v1/property_graph/analytics$people/insert" => {
|
||||
assert_eq!(request.url().query(), Some("label=Person"));
|
||||
assert_eq!(
|
||||
request.headers().get("Content-Type").unwrap(),
|
||||
ARROW_STREAM_CONTENT_TYPE
|
||||
);
|
||||
}
|
||||
"/v1/property_graph/analytics$people/refresh"
|
||||
| "/v1/property_graph/analytics$people/rollback" => {
|
||||
let body = match request.url().path() {
|
||||
"/v1/virtual_property_graph/social/create"
|
||||
| "/v1/virtual_property_graph/social/describe" => graph_description_body(
|
||||
serde_json::to_value(social_graph()).unwrap(),
|
||||
"social",
|
||||
&[],
|
||||
serde_json::json!({}),
|
||||
),
|
||||
"/v1/materialized_virtual_property_graph/social_mv/describe"
|
||||
| "/v1/materialized_virtual_property_graph/social_mv/refresh" => {
|
||||
assert!(request.body().is_none(), "{:?}", request.body());
|
||||
materialized_body("social_mv", &[])
|
||||
}
|
||||
"/v1/property_graph/people/create"
|
||||
| "/v1/property_graph/people/describe"
|
||||
| "/v1/property_graph/people/rollback" => graph_description_body(
|
||||
serde_json::to_value(people_graph()).unwrap(),
|
||||
"people",
|
||||
&[],
|
||||
serde_json::json!({
|
||||
"commit": "c2",
|
||||
"committed_at": "2026-09-26T17:00:00Z",
|
||||
"previous_commit": "c1",
|
||||
"vertex_count": 0,
|
||||
"edge_count": 0,
|
||||
}),
|
||||
),
|
||||
other => panic!("unexpected path {other}"),
|
||||
}
|
||||
};
|
||||
http::Response::builder().status(200).body(body).unwrap()
|
||||
});
|
||||
let virtual_graph = conn
|
||||
.create_virtual_property_graph("social", &social_graph(), &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(virtual_graph.definition, social_graph());
|
||||
assert_eq!(
|
||||
conn.describe_virtual_property_graph("social", &[])
|
||||
.await
|
||||
.unwrap(),
|
||||
virtual_graph
|
||||
);
|
||||
let refreshed = conn
|
||||
.refresh_materialized_virtual_property_graph("social_mv", &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(refreshed.edge_count, 5);
|
||||
conn.describe_materialized_virtual_property_graph("social_mv", &[])
|
||||
.await
|
||||
.unwrap();
|
||||
let people = conn
|
||||
.create_property_graph("people", &people_graph(), &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(people.definition, people_graph());
|
||||
assert_eq!(people.previous_commit.as_deref(), Some("c1"));
|
||||
conn.describe_property_graph("people", &[]).await.unwrap();
|
||||
conn.rollback_property_graph("people", &[]).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_insert_into_property_graph_sends_arrow_rows() {
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::POST);
|
||||
assert_eq!(
|
||||
request.url().path(),
|
||||
"/v1/property_graph/analytics$people/insert"
|
||||
);
|
||||
assert_eq!(request.url().query(), Some("label=Person"));
|
||||
assert_eq!(
|
||||
request.headers().get("Content-Type").unwrap(),
|
||||
ARROW_STREAM_CONTENT_TYPE
|
||||
);
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(property_graph_description_body("people", &["analytics"]))
|
||||
.body(graph_description_body(
|
||||
serde_json::to_value(people_graph()).unwrap(),
|
||||
"people",
|
||||
&["analytics"],
|
||||
serde_json::json!({
|
||||
"commit": "c2",
|
||||
"committed_at": "2026-09-26T17:00:00Z",
|
||||
"vertex_count": 2,
|
||||
"edge_count": 0,
|
||||
}),
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
let namespace = ["analytics".to_string()];
|
||||
let rows = RecordBatch::try_new(
|
||||
Arc::new(Schema::new(vec![Field::new(
|
||||
"person_id",
|
||||
@@ -4556,67 +4806,47 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let inserted = conn
|
||||
.insert_into_property_graph("people", "Person", rows, &namespace)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(inserted.name, "people");
|
||||
conn.refresh_property_graph("people", &namespace)
|
||||
.await
|
||||
.unwrap();
|
||||
conn.rollback_property_graph("people", &namespace)
|
||||
.insert_into_property_graph("people", "Person", rows, &["analytics".to_string()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(inserted.vertex_count, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_describe_and_drop_address_the_property_graph_in_the_path() {
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::POST);
|
||||
assert_eq!(request.url().path(), "/v1/property_graph/social/describe");
|
||||
assert!(request.body().is_none(), "{:?}", request.body());
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(property_graph_description_body("social", &[]))
|
||||
.unwrap()
|
||||
});
|
||||
let graph = conn.describe_property_graph("social", &[]).await.unwrap();
|
||||
assert!(graph.namespace_path.is_empty());
|
||||
let crate::graph::PropertyGraphDefinition::MaterializedView { edges, .. } =
|
||||
graph.definition
|
||||
else {
|
||||
panic!("expected a materialized view graph")
|
||||
};
|
||||
assert_eq!(edges[0].properties, Some(vec!["since".to_string()]));
|
||||
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::POST);
|
||||
assert_eq!(
|
||||
request.url().path(),
|
||||
"/v1/property_graph/analytics$social/drop"
|
||||
);
|
||||
assert!(request.body().is_none(), "{:?}", request.body());
|
||||
http::Response::builder().status(200).body("{}").unwrap()
|
||||
});
|
||||
conn.drop_property_graph("social", &["analytics".into()])
|
||||
.await
|
||||
async fn test_drop_graph_async_reports_the_cleanup_job() {
|
||||
for kind in [
|
||||
"property_graph",
|
||||
"virtual_property_graph",
|
||||
"materialized_virtual_property_graph",
|
||||
] {
|
||||
let db = super::RemoteDatabase::new_mock(move |request| {
|
||||
assert_eq!(request.url().path(), format!("/v1/{kind}/social/drop"));
|
||||
http::Response::builder()
|
||||
.status(202)
|
||||
.body(r#"{"job_id":"j1-do-graph"}"#)
|
||||
.unwrap()
|
||||
});
|
||||
let job = match kind {
|
||||
"property_graph" => db.drop_property_graph_async("social", &[]).await,
|
||||
"virtual_property_graph" => {
|
||||
db.drop_virtual_property_graph_async("social", &[]).await
|
||||
}
|
||||
_ => {
|
||||
db.drop_materialized_virtual_property_graph_async("social", &[])
|
||||
.await
|
||||
}
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drop_property_graph_async_reports_the_cleanup_job() {
|
||||
let db = super::RemoteDatabase::new_mock(|_| {
|
||||
http::Response::builder()
|
||||
.status(202)
|
||||
.body(r#"{"job_id":"j1-do-graph"}"#)
|
||||
.unwrap()
|
||||
});
|
||||
let job = db.drop_property_graph_async("social", &[]).await.unwrap();
|
||||
assert_eq!(job.id(), Some("j1-do-graph"));
|
||||
assert_eq!(job.id(), Some("j1-do-graph"), "{kind}");
|
||||
}
|
||||
|
||||
let db = super::RemoteDatabase::new_mock(|_| {
|
||||
http::Response::builder().status(200).body("{}").unwrap()
|
||||
});
|
||||
let job = db.drop_property_graph_async("social", &[]).await.unwrap();
|
||||
let job = db
|
||||
.drop_virtual_property_graph_async("social", &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(job.id(), None);
|
||||
job.wait().await.unwrap();
|
||||
|
||||
@@ -4637,12 +4867,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_property_graphs_walks_pages() {
|
||||
async fn test_list_graphs_walks_pages_of_their_kind() {
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(request.method(), &reqwest::Method::GET);
|
||||
assert_eq!(
|
||||
request.url().path(),
|
||||
"/v1/namespace/analytics/property_graph/list"
|
||||
"/v1/namespace/analytics/materialized_virtual_property_graph/list"
|
||||
);
|
||||
let page = request
|
||||
.url()
|
||||
@@ -4650,23 +4880,40 @@ mod tests {
|
||||
.find(|(key, _)| key == "page_token")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
let body = match page.as_deref() {
|
||||
None => r#"{"property_graphs":["social"],"page_token":"p2"}"#,
|
||||
Some("p2") => r#"{"property_graphs":["payments"],"page_token":null}"#,
|
||||
None => r#"{"materialized_virtual_property_graphs":["social"],"page_token":"p2"}"#,
|
||||
Some("p2") => {
|
||||
r#"{"materialized_virtual_property_graphs":["payments"],"page_token":null}"#
|
||||
}
|
||||
Some(other) => panic!("unexpected page token: {other}"),
|
||||
};
|
||||
http::Response::builder().status(200).body(body).unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
conn.list_property_graphs(&["analytics".into()])
|
||||
conn.list_materialized_virtual_property_graphs(&["analytics".into()])
|
||||
.await
|
||||
.unwrap(),
|
||||
vec!["social".to_string(), "payments".to_string()]
|
||||
);
|
||||
|
||||
let conn = Connection::new_with_handler(|request| {
|
||||
assert_eq!(
|
||||
request.url().path(),
|
||||
"/v1/namespace/$/virtual_property_graph/list"
|
||||
);
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(r#"{"virtual_property_graphs":["social"]}"#)
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
conn.list_virtual_property_graphs(&[]).await.unwrap(),
|
||||
vec!["social".to_string()]
|
||||
);
|
||||
|
||||
let conn = Connection::new_with_handler(|_| {
|
||||
http::Response::builder()
|
||||
.status(200)
|
||||
.body(r#"{"property_graphs":["social"],"page_token":"same"}"#)
|
||||
.body(r#"{"property_graphs":["people"],"page_token":"same"}"#)
|
||||
.unwrap()
|
||||
});
|
||||
let error = conn.list_property_graphs(&[]).await.unwrap_err();
|
||||
@@ -4689,7 +4936,7 @@ mod tests {
|
||||
("", vec![]),
|
||||
("..", vec![]),
|
||||
] {
|
||||
let error = match conn.describe_property_graph(name, &namespace).await {
|
||||
let error = match conn.describe_virtual_property_graph(name, &namespace).await {
|
||||
Ok(_) => panic!("accepted {name:?} in {namespace:?}"),
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user