Skip to content

Working with Multiple Graphs

This content is for v0.10.0. Switch to the latest version for up-to-date documentation.

GraphQLite supports managing and querying across multiple graph databases. This is useful for:

  • Separation of concerns: Keep different data domains in separate graphs.
  • Access control: Different graphs can have different file-level permissions.
  • Performance: Smaller, focused graphs are faster to query and analyze.
  • Cross-domain queries: Join relationships that span different datasets.

The GraphManager class (accessed via the graphs() factory function) manages multiple graph databases stored as separate SQLite files in a directory.

from graphqlite import graphs
with graphs("./data") as gm:
# Create new graphs (raises FileExistsError if already exists)
social = gm.create("social")
products = gm.create("products")
# Populate them
social.upsert_node("alice", {"name": "Alice", "age": 30, "user_id": "u1"}, "Person")
social.upsert_node("bob", {"name": "Bob", "age": 25, "user_id": "u2"}, "Person")
social.upsert_edge("alice", "bob", {"since": 2020}, "KNOWS")
products.upsert_node("phone", {"name": "iPhone 15", "price": 999}, "Product")
products.upsert_node("laptop", {"name": "MacBook Pro", "price": 1999}, "Product")
print(gm.list()) # ['products', 'social']
print(len(gm)) # 2
print("social" in gm) # True

All connections are closed automatically when the with block exits.

from graphqlite import graphs
with graphs("./data") as gm:
# Open an existing graph (raises FileNotFoundError if missing)
social = gm.open("social")
# Open, creating if it doesn't exist
cache = gm.open_or_create("cache")
for row in social.query("MATCH (n:Person) RETURN n.name ORDER BY n.name"):
print(row["n.name"])
from graphqlite import graphs
with graphs("./data") as gm:
# List all graph names in the directory
for name in gm.list():
g = gm.open(name)
print(f"{name}: {g.stats()}")
# Delete a graph and its database file permanently
gm.drop("cache")

drop() deletes the .db file on disk. There is no undo.

GraphQLite can query across multiple graphs in a single Cypher statement using the FROM clause.

Attach one or more graphs and reference them by name in MATCH patterns:

from graphqlite import graphs
with graphs("./data") as gm:
social = gm.open_or_create("social")
social.upsert_node("alice", {"name": "Alice", "user_id": "u1"}, "Person")
purchases = gm.open_or_create("purchases")
purchases.upsert_node("order1", {"user_id": "u1", "total": 99.99, "item": "Phone"}, "Order")
# GraphManager commits open graphs before running cross-graph queries
result = gm.query(
"""
MATCH (p:Person) FROM social
WHERE p.user_id = 'u1'
RETURN p.name, graph(p) AS source
""",
graphs=["social"]
)
for row in result:
print(f"{row['p.name']} is from graph: {row['source']}")

The graphs parameter tells gm.query() which databases to attach before running the query.

graph(node) returns the name of the graph that the node comes from. Use it to identify results in multi-graph queries:

result = gm.query(
"""
MATCH (n) FROM social
RETURN n.name, graph(n) AS source_graph
""",
graphs=["social"]
)
for row in result:
print(f"{row['n.name']} lives in {row['source_graph']}")

Pass parameters to cross-graph queries the same way as single-graph queries:

result = gm.query(
"MATCH (n:Person {user_id: $uid}) FROM social RETURN n.name",
graphs=["social"],
params={"uid": "u1"}
)

For low-level access, query_sql() attaches the named graphs and runs raw SQL. The attached graph’s tables are prefixed with the graph name:

# Count nodes in the social graph
result = gm.query_sql(
"SELECT COUNT(*) AS node_count FROM social.nodes",
graphs=["social"]
)
print(f"Social graph has {result[0][0]} nodes")
# Join across graphs with raw SQL
result = gm.query_sql(
"""
SELECT s.user_id, COUNT(p.rowid) AS order_count
FROM social.node_props_text s
JOIN purchases.node_props_text p ON s.value = p.value
WHERE s.key = 'user_id' AND p.key = 'user_id'
GROUP BY s.user_id
""",
graphs=["social", "purchases"]
)

query_sql() is useful for analytics that go beyond what Cypher exposes, such as aggregations across multiple graph schemas at once.

Important: Commit Before Cross-Graph Queries

Section titled “Important: Commit Before Cross-Graph Queries”

GraphManager automatically commits all open graph connections before running cross-graph queries with query() or query_sql(). If you are using the underlying connections directly, commit first:

social.connection.execute("COMMIT")
result = gm.query("MATCH (n) FROM social RETURN n", graphs=["social"])

The Rust API mirrors the Python API closely.

use graphqlite::graphs;
fn main() -> graphqlite::Result<()> {
let mut gm = graphs("./data")?;
// Create graphs
{
let social = gm.create("social")?;
social.query("CREATE (n:Person {name: 'Alice', user_id: 'u1'})")?;
social.query("CREATE (n:Person {name: 'Bob', user_id: 'u2'})")?;
social.query(
"MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) \
CREATE (a)-[:KNOWS {since: 2020}]->(b)"
)?;
}
{
let products = gm.create("products")?;
products.query("CREATE (n:Product {name: 'Phone', sku: 'p1', price: 999})")?;
}
// List all graphs
for name in gm.list()? {
println!("Graph: {}", name);
}
// Open an existing graph
let social = gm.open_graph("social")?;
let stats = social.stats()?;
println!("Social: {} nodes, {} edges", stats.nodes, stats.edges);
// Cross-graph query using FROM clause
let result = gm.query(
"MATCH (n:Person) FROM social RETURN n.name AS name ORDER BY n.name",
&["social"],
)?;
for row in &result {
println!("Person: {}", row.get::<String>("name")?);
}
// Raw SQL cross-graph query
let counts = gm.query_sql(
"SELECT COUNT(*) FROM social.nodes",
&["social"],
)?;
// Open or create (idempotent)
let _cache = gm.open_or_create("cache")?;
// Drop a graph
gm.drop("products")?;
// Error handling for missing graphs
match gm.open_graph("nonexistent") {
Err(graphqlite::Error::GraphNotFound { name, available }) => {
println!("'{}' not found. Available: {:?}", name, available);
}
_ => {}
}
Ok(())
}
Method Description
gm.create("name") Create a new graph; errors if it already exists
gm.open_graph("name") Open an existing graph; errors if missing
gm.open_or_create("name") Open or create idempotently
gm.list()? Returns Vec<String> of graph names
gm.exists("name") Returns bool
gm.drop("name")? Delete graph and its file
gm.query(cypher, graphs)? Cross-graph Cypher query
gm.query_sql(sql, graphs)? Cross-graph raw SQL query

For complete control, attach databases manually using SQLite’s ATTACH mechanism:

import sqlite3
import graphqlite
# Build each graph
conn1 = sqlite3.connect("social.db")
graphqlite.load(conn1)
conn1.execute("SELECT cypher('CREATE (n:Person {name: \"Alice\"})')")
conn1.commit()
conn1.close()
conn2 = sqlite3.connect("products.db")
graphqlite.load(conn2)
conn2.execute("SELECT cypher('CREATE (n:Product {name: \"Phone\"})')")
conn2.commit()
conn2.close()
# Query across both
coordinator = sqlite3.connect(":memory:")
graphqlite.load(coordinator)
coordinator.execute("ATTACH DATABASE 'social.db' AS social")
coordinator.execute("ATTACH DATABASE 'products.db' AS products")
result = coordinator.execute(
"SELECT cypher('MATCH (n:Person) FROM social RETURN n.name')"
).fetchall()
print(result)
  1. Use the context manager. with graphs(...) as gm: ensures all connections are closed and any pending transactions are flushed.

  2. Commit before cross-graph queries. GraphManager handles this automatically, but manual connections do not.

  3. Use valid SQL identifiers for graph names. Graph names become SQLite database aliases (ATTACH ... AS name). Use lowercase letters, digits, and underscores only — no hyphens or spaces.

  4. Keep graphs focused. Design each graph around a single domain or service boundary. Cross-graph queries are read-only for the attached graphs.

  5. Use the same extension version. All attached graphs should be queried with the same version of the GraphQLite extension to avoid schema incompatibilities.

  • Cross-graph FROM clause queries are read-only for attached graphs.
  • The FROM clause is only supported inside MATCH patterns.
  • SQLite supports a maximum of approximately 10 simultaneously attached databases.
  • Graph names must be valid SQL identifiers (alphanumeric and underscores).