Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion flowquery-py/src/graph/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Dict, Optional, Union
from typing import Any, Dict, Optional, Union

from ..parsing.ast_node import ASTNode
from .node import Node
Expand Down Expand Up @@ -54,6 +54,30 @@ def get_relationship(self, relationship: 'Relationship') -> Optional['PhysicalRe
"""Gets a relationship from the database."""
return Database._relationships.get(relationship.type) if relationship.type else None

async def schema(self) -> list[dict[str, Any]]:
"""Returns the graph schema with node/relationship labels and sample data."""
result: list[dict[str, Any]] = []

for label, physical_node in Database._nodes.items():
records = await physical_node.data()
entry: dict[str, Any] = {"kind": "node", "label": label}
if records:
sample = {k: v for k, v in records[0].items() if k != "id"}
if sample:
entry["sample"] = sample
result.append(entry)

for rel_type, physical_rel in Database._relationships.items():
records = await physical_rel.data()
entry_rel: dict[str, Any] = {"kind": "relationship", "type": rel_type}
if records:
sample = {k: v for k, v in records[0].items() if k not in ("left_id", "right_id")}
if sample:
entry_rel["sample"] = sample
result.append(entry_rel)

return result

async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']:
"""Gets data for a node or relationship."""
if isinstance(element, Node):
Expand Down
2 changes: 2 additions & 0 deletions flowquery-py/src/parsing/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .reducer_element import ReducerElement
from .replace import Replace
from .round_ import Round
from .schema import Schema
from .size import Size
from .split import Split
from .stringify import Stringify
Expand Down Expand Up @@ -71,5 +72,6 @@
"ToJson",
"Type",
"Functions",
"Schema",
"PredicateSum",
]
36 changes: 36 additions & 0 deletions flowquery-py/src/parsing/functions/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Schema introspection function."""

from typing import Any, AsyncGenerator

from .async_function import AsyncFunction
from .function_metadata import FunctionDef


@FunctionDef({
"description": (
"Returns the graph schema listing all nodes and relationships "
"with a sample of their data."
),
"category": "async",
"parameters": [],
"output": {
"description": "Schema entry with kind, label/type, and optional sample data",
"type": "object",
},
"examples": [
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample",
],
})
class Schema(AsyncFunction):
"""Returns the graph schema of the database.

Lists all nodes and relationships with their labels/types and a sample
of their data (excluding id from nodes, left_id and right_id from relationships).
"""

async def generate(self) -> AsyncGenerator[Any, None]:
# Import at runtime to avoid circular dependency
from ...graph.database import Database
entries = await Database.get_instance().schema()
for entry in entries:
yield entry
47 changes: 46 additions & 1 deletion flowquery-py/tests/compute/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1539,4 +1539,49 @@ async def test_match_with_constraints(self):
await match.run()
results = match.results
assert len(results) == 1
assert results[0]["name"] == "Employee 1"
assert results[0]["name"] == "Employee 1"

@pytest.mark.asyncio
async def test_schema_returns_nodes_and_relationships_with_sample_data(self):
"""Test schema() returns nodes and relationships with sample data."""
await Runner(
"""
CREATE VIRTUAL (:Animal) AS {
UNWIND [
{id: 1, species: 'Cat', legs: 4},
{id: 2, species: 'Dog', legs: 4}
] AS record
RETURN record.id AS id, record.species AS species, record.legs AS legs
}
"""
).run()
await Runner(
"""
CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
UNWIND [
{left_id: 2, right_id: 1, speed: 'fast'}
] AS record
RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
}
"""
).run()

runner = Runner(
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
)
await runner.run()
results = runner.results

animal = next((r for r in results if r.get("kind") == "node" and r.get("label") == "Animal"), None)
assert animal is not None
assert animal["sample"] is not None
assert "id" not in animal["sample"]
assert "species" in animal["sample"]
assert "legs" in animal["sample"]

chases = next((r for r in results if r.get("kind") == "relationship" and r.get("type") == "CHASES"), None)
assert chases is not None
assert chases["sample"] is not None
assert "left_id" not in chases["sample"]
assert "right_id" not in chases["sample"]
assert "speed" in chases["sample"]
30 changes: 30 additions & 0 deletions src/graph/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,36 @@ class Database {
public getRelationship(relationship: Relationship): PhysicalRelationship | null {
return Database.relationships.get(relationship.type!) || null;
}
public async schema(): Promise<Record<string, any>[]> {
const result: Record<string, any>[] = [];

for (const [label, physical] of Database.nodes) {
const records = await physical.data();
const entry: Record<string, any> = { kind: "node", label };
if (records.length > 0) {
const { id, ...sample } = records[0];
if (Object.keys(sample).length > 0) {
entry.sample = sample;
}
}
result.push(entry);
}

for (const [type, physical] of Database.relationships) {
const records = await physical.data();
const entry: Record<string, any> = { kind: "relationship", type };
if (records.length > 0) {
const { left_id, right_id, ...sample } = records[0];
if (Object.keys(sample).length > 0) {
entry.sample = sample;
}
}
result.push(entry);
}

return result;
}

public async getData(element: Node | Relationship): Promise<NodeData | RelationshipData> {
if (element instanceof Node) {
const node = this.getNode(element);
Expand Down
1 change: 1 addition & 0 deletions src/parsing/functions/function_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import "./rand";
import "./range";
import "./replace";
import "./round";
import "./schema";
import "./size";
import "./split";
import "./stringify";
Expand Down
36 changes: 36 additions & 0 deletions src/parsing/functions/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Database from "../../graph/database";
import AsyncFunction from "./async_function";
import { FunctionDef } from "./function_metadata";

/**
* Built-in function that returns the graph schema of the database.
*
* Lists all nodes and relationships with their labels/types and a sample
* of their data (excluding id from nodes, left_id and right_id from relationships).
*
* @example
* ```
* LOAD FROM schema() AS s RETURN s
* ```
*/
@FunctionDef({
description:
"Returns the graph schema listing all nodes and relationships with a sample of their data.",
category: "async",
parameters: [],
output: {
description: "Schema entry with kind, label/type, and optional sample data",
type: "object",
},
examples: ["LOAD FROM schema() AS s RETURN s"],
})
class Schema extends AsyncFunction {
public async *generate(): AsyncGenerator<any> {
const entries = await Database.getInstance().schema();
for (const entry of entries) {
yield entry;
}
}
}

export default Schema;
40 changes: 40 additions & 0 deletions tests/compute/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1460,3 +1460,43 @@ test("Test match with leftward double graph pattern", async () => {
expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 2", name3: "Person 3" });
expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 3", name3: "Person 4" });
});

test("Test schema() returns nodes and relationships with sample data", async () => {
await new Runner(`
CREATE VIRTUAL (:Animal) AS {
UNWIND [
{id: 1, species: 'Cat', legs: 4},
{id: 2, species: 'Dog', legs: 4}
] AS record
RETURN record.id AS id, record.species AS species, record.legs AS legs
}
`).run();
await new Runner(`
CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
UNWIND [
{left_id: 2, right_id: 1, speed: 'fast'}
] AS record
RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
}
`).run();

const runner = new Runner(
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
);
await runner.run();
const results = runner.results;

const animal = results.find((r: any) => r.kind === "node" && r.label === "Animal");
expect(animal).toBeDefined();
expect(animal.sample).toBeDefined();
expect(animal.sample).not.toHaveProperty("id");
expect(animal.sample).toHaveProperty("species");
expect(animal.sample).toHaveProperty("legs");

const chases = results.find((r: any) => r.kind === "relationship" && r.type === "CHASES");
expect(chases).toBeDefined();
expect(chases.sample).toBeDefined();
expect(chases.sample).not.toHaveProperty("left_id");
expect(chases.sample).not.toHaveProperty("right_id");
expect(chases.sample).toHaveProperty("speed");
});
Loading