Working with Multiple Graphs
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.
Python: GraphManager
Section titled “Python: GraphManager”The GraphManager class (accessed via the graphs() factory function) manages multiple graph databases stored as separate SQLite files in a directory.
Creating and Opening Graphs
Section titled “Creating and Opening Graphs”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) # TrueAll connections are closed automatically when the with block exits.
Opening Existing Graphs
Section titled “Opening Existing Graphs”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"])Listing and Dropping Graphs
Section titled “Listing and Dropping Graphs”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.
Cross-Graph Queries
Section titled “Cross-Graph Queries”GraphQLite can query across multiple graphs in a single Cypher statement using the FROM clause.
The FROM Clause
Section titled “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.
The graph() Function
Section titled “The graph() Function”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']}")Cross-Graph Queries with Parameters
Section titled “Cross-Graph Queries with Parameters”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"})Raw SQL Cross-Graph Queries
Section titled “Raw SQL Cross-Graph Queries”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 graphresult = 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 SQLresult = 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"])Rust: GraphManager
Section titled “Rust: GraphManager”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(())}Rust GraphManager Methods
Section titled “Rust GraphManager Methods”| 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 |
Using ATTACH Directly
Section titled “Using ATTACH Directly”For complete control, attach databases manually using SQLite’s ATTACH mechanism:
import sqlite3import graphqlite
# Build each graphconn1 = 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 bothcoordinator = 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)Best Practices
Section titled “Best Practices”-
Use the context manager.
with graphs(...) as gm:ensures all connections are closed and any pending transactions are flushed. -
Commit before cross-graph queries. GraphManager handles this automatically, but manual connections do not.
-
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. -
Keep graphs focused. Design each graph around a single domain or service boundary. Cross-graph queries are read-only for the attached graphs.
-
Use the same extension version. All attached graphs should be queried with the same version of the GraphQLite extension to avoid schema incompatibilities.
Limitations
Section titled “Limitations”- Cross-graph
FROMclause queries are read-only for attached graphs. - The
FROMclause is only supported insideMATCHpatterns. - SQLite supports a maximum of approximately 10 simultaneously attached databases.
- Graph names must be valid SQL identifiers (alphanumeric and underscores).