graph_kit 0.7.4
graph_kit: ^0.7.4 copied to clipboard
A lightweight, in-memory graph library with pattern-based queries and efficient traversal for Dart and Flutter applications.
graph kit — lightweight typed directed multigraph + pattern queries
In-memory, typed directed multigraph with powerful and performant Cypher-inspired pattern queries
In-memory, typed directed multigraph with:
- Typed nodes (e.g.,
Person
,Team
,Project
,Resource
) - Typed edges (e.g.,
WORKS_FOR
,MANAGES
,ASSIGNED_TO
,DEPENDS_ON
) - Multiple relationships between the same nodes
- Advanced Cypher queries with WHERE clauses, RETURN projection, logical operators, and variable-length paths
- Complete path results with Neo4j-style edge information
- Graph algorithms for analysis (shortest path, connected components, topological sort, reachability)
Table of Contents #
- 1. Quick Preview
- 2. Complete Usage Examples
- 3. Graph Algorithms
- 4. Generic Traversal Utilities
- 5. Pattern Query Examples
- 6. Mini-Cypher Reference
- 7. Comparison with Cypher
- 8. Design and performance
- 9. JSON Serialization
- 10. Examples index
- License
1. Quick Preview #
Interactive graph algorithms demo showing centrality analysis in the Flutter app.
2. Complete Usage Examples #
This section provides copy-paste ready examples demonstrating all major query methods with a sample graph. Each example can be run as a standalone Dart script.
2.1 Setup: Sample Graph #
import 'package:graph_kit/graph_kit.dart';
void main() {
// Create graph and add sample data
final graph = Graph<Node>();
// Add people
graph.addNode(Node(id: 'alice', type: 'Person', label: 'Alice Cooper'));
graph.addNode(Node(id: 'bob', type: 'Person', label: 'Bob Wilson'));
graph.addNode(Node(id: 'charlie', type: 'Person', label: 'Charlie Davis'));
// Add teams
graph.addNode(Node(id: 'engineering', type: 'Team', label: 'Engineering'));
graph.addNode(Node(id: 'design', type: 'Team', label: 'Design Team'));
graph.addNode(Node(id: 'marketing', type: 'Team', label: 'Marketing'));
// Add projects
graph.addNode(Node(id: 'web_app', type: 'Project', label: 'Web Application'));
graph.addNode(Node(id: 'mobile_app', type: 'Project', label: 'Mobile App'));
graph.addNode(Node(id: 'campaign', type: 'Project', label: 'Ad Campaign'));
// Add relationships
graph.addEdge('alice', 'WORKS_FOR', 'engineering');
graph.addEdge('bob', 'WORKS_FOR', 'engineering');
graph.addEdge('charlie', 'MANAGES', 'engineering');
graph.addEdge('charlie', 'MANAGES', 'design');
graph.addEdge('charlie', 'MANAGES', 'marketing');
graph.addEdge('engineering', 'ASSIGNED_TO', 'web_app');
graph.addEdge('engineering', 'ASSIGNED_TO', 'mobile_app');
graph.addEdge('design', 'ASSIGNED_TO', 'mobile_app');
graph.addEdge('marketing', 'ASSIGNED_TO', 'campaign');
graph.addEdge('alice', 'LEADS', 'web_app');
final query = PatternQuery(graph);
// Run examples below...
}
2.2 Basic Queries - Get Single Type #
// Get all people, query.match returns Map<String, Set<String>>
final people = query.match('person:Person');
print(people); // {person: {alice, bob, charlie}}
// Get all teams, query.match returns Map<String, Set<String>>
final teams = query.match('team:Team');
print(teams); // {team: {engineering, design, marketing}}
// Get all projects, query.match returns Map<String, Set<String>>
final projects = query.match('project:Project');
print(projects); // {project: {web_app, mobile_app, campaign}}
2.3 Relationship Queries - Get Connected Nodes #
// Find who works for teams, query.match returns Map<String, Set<String>>
final workers = query.match('person:Person-[:WORKS_FOR]->team:Team');
print(workers); // {person: {alice, bob}, team: {engineering}}
// Find who manages teams, query.match returns Map<String, Set<String>>
final managers = query.match('person:Person-[:MANAGES]->team:Team');
print(managers); // {person: {charlie}, team: {engineering, design, marketing}}
// Find team assignments to projects, query.match returns Map<String, Set<String>>
final assignments = query.match('team:Team-[:ASSIGNED_TO]->project:Project');
print(assignments); // {team: {engineering, design, marketing}, project: {web_app, mobile_app, campaign}}
2.4 Queries from Specific Starting Points #
// What does Alice work on? query.match with startId returns Map<String, Set<String>>
final aliceWork = query.match(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'alice'
);
print(aliceWork); // {person: {alice}, team: {engineering}, project: {web_app, mobile_app}}
// What does Charlie manage? query.match with startId returns Map<String, Set<String>>
final charlieManages = query.match(
'person-[:MANAGES]->team',
startId: 'charlie'
);
print(charlieManages); // {person: {charlie}, team: {engineering, design, marketing}}
// Who works on the web app project? query.match with startId returns Map<String, Set<String>>
final webAppTeam = query.match(
'project<-[:ASSIGNED_TO]-team<-[:WORKS_FOR]-person',
startId: 'web_app'
);
print(webAppTeam); // {project: {web_app}, team: {engineering}, person: {alice, bob}}
2.5 Row-wise Results - Preserve Path Relationships #
// Get specific person-team-project combinations, query.matchRows returns List<Map<String, String>>
final rows = query.matchRows('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project');
print(rows);
// [
// {person: alice, team: engineering, project: web_app},
// {person: alice, team: engineering, project: mobile_app},
// {person: bob, team: engineering, project: web_app},
// {person: bob, team: engineering, project: mobile_app}
// ]
// Access individual path data
print(rows.first); // {person: alice, team: engineering, project: web_app}
print(rows.first['person']); // alice
print(rows.first['team']); // engineering
2.6 Complete Path Results with Edge Information #
// Get complete path information, query.matchPaths returns List<PathMatch>
final paths = query.matchPaths('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project');
print(paths.length); // 4
// Print all paths with edges
for (final path in paths) {
print(path.nodes); // {person: alice, team: engineering, project: web_app}
for (final edge in path.edges) {
print(' ${edge.from} -[:${edge.type}]-> ${edge.to}'); // alice -[:WORKS_FOR]-> engineering
}
}
2.7 Multiple Pattern Queries #
// Get all of Alice's connections, query.matchMany returns Map<String, Set<String>>
final aliceConnections = query.matchMany([
'person-[:WORKS_FOR]->team',
'person-[:LEADS]->project'
], startId: 'alice');
print(aliceConnections); // {person: {alice}, team: {engineering}, project: {web_app}}
// Combine multiple relationship types, query.matchMany returns Map<String, Set<String>>
final allConnections = query.matchMany([
'person:Person-[:WORKS_FOR]->team:Team',
'person:Person-[:MANAGES]->team:Team',
'person:Person-[:LEADS]->project:Project'
]);
print(allConnections); // {person: {alice, bob, charlie}, team: {engineering, design, marketing}, project: {web_app}}
2.8 WHERE Clause Filtering #
// Add people with properties for filtering examples
graph.addNode(Node(
id: 'alice',
type: 'Person',
label: 'Alice Cooper',
properties: {'age': 28, 'department': 'Engineering', 'salary': 85000}
));
graph.addNode(Node(
id: 'bob',
type: 'Person',
label: 'Bob Wilson',
properties: {'age': 35, 'department': 'Engineering', 'salary': 95000}
));
// Filter by age - query.matchRows returns List<Map<String, String>>
final seniors = query.matchRows('MATCH person:Person WHERE person.age > 30');
print(seniors); // [{person: bob}]
// Filter by department - query.matchRows returns List<Map<String, String>>
final engineers = query.matchRows('MATCH person:Person WHERE person.department = "Engineering"');
print(engineers); // [{person: alice}, {person: bob}]
// Combine conditions with AND - query.matchRows returns List<Map<String, String>>
final seniorEngineers = query.matchRows('MATCH person:Person WHERE person.age > 30 AND person.department = "Engineering"');
print(seniorEngineers); // [{person: bob}]
// Use OR conditions - query.matchRows returns List<Map<String, String>>
final youngOrWellPaid = query.matchRows('MATCH person:Person WHERE person.age < 30 OR person.salary > 90000');
print(youngOrWellPaid); // [{person: alice}, {person: bob}]
// Complex filtering with relationships - query.matchRows returns List<Map<String, String>>
final seniorWorkers = query.matchRows('MATCH person:Person-[:WORKS_FOR]->team:Team WHERE person.age > 30');
print(seniorWorkers); // [{person: bob, team: engineering}]
2.9 RETURN Clause - Property Projection #
The RETURN clause lets you project specific variables and properties, creating clean, production-ready result sets instead of raw node IDs.
Why use RETURN?
- Clean data: Get only what you need (no extra lookups)
- Custom names: Use AS aliases for readable column names
- Type-safe patterns: Combine with Dart 3 destructuring
- Performance: Reduces data transfer and processing
Basic RETURN - Variable Projection
// Without RETURN: Get node IDs (requires separate lookups)
final rawResults = query.matchRows('MATCH person:Person-[:WORKS_FOR]->team:Team');
print(rawResults); // [{person: alice, team: engineering}, ...]
// With RETURN: Get only what you need
final results = query.matchRows('MATCH person:Person-[:WORKS_FOR]->team:Team RETURN person, team');
print(results); // [{person: alice, team: engineering}, ...]
// Same format, but explicitly controlled
Property Access - Get Actual Data
// RETURN properties directly from nodes
final employeeData = query.matchRows(
'MATCH person:Person-[:WORKS_FOR]->team:Team RETURN person.name, person.salary, team.name'
);
print(employeeData);
// [
// {'person.name': 'Alice Cooper', 'person.salary': 85000, 'team.name': 'Engineering'},
// {'person.name': 'Bob Wilson', 'person.salary': 95000, 'team.name': 'Engineering'}
// ]
// Access values
print(employeeData.first['person.name']); // Alice Cooper
print(employeeData.first['person.salary']); // 85000
AS Aliases - Custom Column Names
// Use AS to create readable column names
final cleanResults = query.matchRows(
'MATCH person:Person-[:WORKS_FOR]->team:Team '
'RETURN person.name AS employee, person.salary AS pay, team.name AS department'
);
print(cleanResults);
// [
// {employee: 'Alice Cooper', pay: 85000, department: 'Engineering'},
// {employee: 'Bob Wilson', pay: 95000, department: 'Engineering'}
// ]
// Much cleaner access!
print(cleanResults.first['employee']); // Alice Cooper
print(cleanResults.first['department']); // Engineering
Destructuring - Type-Safe Access (Dart 3)
Use Dart's pattern matching to destructure results type-safely:
// Destructure in a loop
final results = query.matchRows(
'MATCH person:Person WHERE person.salary > 90000 '
'RETURN person.name AS name, person.salary AS salary, person.department AS dept'
);
for (var {'name': employeeName, 'salary': pay, 'dept': department} in results) {
print('$employeeName earns \$$pay in $department');
}
// Output:
// Bob Wilson earns $95000 in Engineering
Shorter syntax for single result:
final topEarner = query.matchRows(
'MATCH person:Person RETURN person.name AS name, person.salary AS salary'
).first;
var {'name': name, 'salary': salary} = topEarner;
print('$name: \$$salary'); // Uses destructured variables directly
Combining WHERE + RETURN
// Filter with WHERE, then project with RETURN
final seniorEngineers = query.matchRows(
'MATCH person:Person-[:WORKS_FOR]->team:Team '
'WHERE person.age > 30 AND team.name = "Engineering" '
'RETURN person.name AS engineer, person.age AS yearsOld, team.name AS teamName'
);
for (var {'engineer': name, 'yearsOld': age} in seniorEngineers) {
print('$name ($age years old)');
}
Real-World Example - Employee Directory
// Complete employee report query
final report = query.matchRows(
'MATCH person:Person-[:WORKS_FOR]->team:Team-[:WORKS_ON]->project:Project '
'WHERE person.salary >= 85000 '
'RETURN person.name AS employee, '
' person.role AS title, '
' person.salary AS compensation, '
' team.name AS department, '
' project.name AS currentProject'
);
// Use destructuring for clean output
for (var {
'employee': name,
'title': role,
'compensation': salary,
'department': dept,
'currentProject': project
} in report) {
print('$name ($role) - $dept - \$$salary - Working on: $project');
}
// Output:
// Alice Cooper (Senior Engineer) - Engineering - $85000 - Working on: Web Application
// Bob Wilson (Staff Engineer) - Engineering - $95000 - Working on: Mobile App
RETURN vs Raw IDs - Quick Comparison
Approach | Result Format | Use Case |
---|---|---|
No RETURN | Node IDs only | When you need to hydrate objects elsewhere |
RETURN variables | {person: 'alice', team: 'engineering'} |
ID-based hydration pattern |
RETURN properties | {'person.name': 'Alice', 'team.name': 'Engineering'} |
Direct property access |
RETURN with AS | {employee: 'Alice', dept: 'Engineering'} |
Clean, production-ready data |
Try the interactive demo:
cd example
flutter run
# Select "RETURN Clause Projection" to see live examples
2.10 Utility Methods #
// Find by type, query.findByType returns Set<String>
final allPeople = query.findByType('Person');
print(allPeople); // {alice, bob, charlie}
// Find by exact label, query.findByLabelEquals returns Set<String>
final aliceIds = query.findByLabelEquals('Alice Cooper');
print(aliceIds); // {alice}
// Find by label substring, query.findByLabelContains returns Set<String>
final bobUsers = query.findByLabelContains('bob');
print(bobUsers); // {bob}
// Direct edge traversal, query.outFrom returns Set<String>
final aliceTeams = query.outFrom('alice', 'WORKS_FOR');
print(aliceTeams); // {engineering}
// Reverse edge traversal, query.inTo returns Set<String>
final engineeringWorkers = query.inTo('engineering', 'WORKS_FOR');
print(engineeringWorkers); // {alice, bob}
2.11 Summary of Query Methods #
Method | Returns | Use Case |
---|---|---|
match() |
Map<String, Set<String>> |
Get grouped node IDs by variable |
matchMany() |
Map<String, Set<String>> |
Combine multiple patterns |
matchRows() |
List<Map<String, String>> |
Preserve path relationships |
matchPaths() |
List<PathMatch> |
Complete path + edge information |
findByType() |
Set<String> |
All nodes of specific type |
findByLabelEquals() |
Set<String> |
Nodes by exact label match |
findByLabelContains() |
Set<String> |
Nodes by label substring |
outFrom() /inTo() |
Set<String> |
Direct edge traversal |
3. Graph Algorithms #
Graph Kit includes efficient implementations of common graph algorithms for analysis and pathfinding:
Available Algorithms #
- Shortest Path - Find optimal routes between nodes using BFS (counts hops)
- Path Enumeration - Find all possible routes between nodes within hop limits
- Connected Components - Identify groups of interconnected nodes
- Reachability Analysis - Discover all nodes reachable from a starting point
- Topological Sort - Order nodes by dependencies (useful for build systems, task scheduling)
- Centrality Analysis - Identify important nodes (betweenness and closeness centrality)
Shortest path visualization highlighting the optimal route between selected nodes.
Quick Example #
import 'package:graph_kit/graph_kit.dart';
// Create a dependency graph
final graph = Graph<Node>();
graph.addNode(Node(id: 'core', type: 'Package', label: 'Core'));
graph.addNode(Node(id: 'utils', type: 'Package', label: 'Utils'));
graph.addNode(Node(id: 'app', type: 'Package', label: 'App'));
// Add dependencies (app depends on utils, utils depends on core)
graph.addEdge('utils', 'DEPENDS_ON', 'core');
graph.addEdge('app', 'DEPENDS_ON', 'utils');
// Use graph algorithms
final algorithms = GraphAlgorithms(graph);
// Find shortest path (counts hops)
final path = algorithms.shortestPath('app', 'core');
print('Path: ${path.path}'); // [app, utils, core]
print('Distance: ${path.distance}'); // 2
// Find all possible paths
final allPaths = enumeratePaths(graph, 'app', 'core', maxHops: 4);
print('Routes: ${allPaths.paths.length}'); // All alternative routes
// Get build order (topological sort)
final buildOrder = algorithms.topologicalSort();
print('Build order: $buildOrder'); // [core, utils, app]
// Find connected components
final components = algorithms.connectedComponents();
print('Components: $components'); // [{core, utils, app}]
// Check reachability
final reachable = algorithms.reachableFrom('app');
print('App can reach: $reachable'); // {app, utils, core}
// Check what can reach a target
final reachableBy = algorithms.reachableBy('core');
print('Can reach core: $reachableBy'); // {app, utils, core}
// Check all connected nodes (bidirectional)
final reachableAll = algorithms.reachableAll('utils');
print('Connected to utils: $reachableAll'); // {app, utils, core}
// Find critical bridge nodes
final betweenness = algorithms.betweennessCentrality();
print('Bridge nodes: ${betweenness.entries.where((e) => e.value > 0.3).map((e) => e.key)}');
// Find communication hubs
final closeness = algorithms.closenessCentrality();
print('Most central: ${closeness.entries.reduce((a, b) => a.value > b.value ? a : b).key}');
Visual Demo #
Run the interactive Flutter demo to see graph algorithms in action:
cd example
flutter run
- Switch to "Graph Algorithms" tab
- Click nodes to see shortest paths
- View connected components with color coding
- See topological sort with dependency levels
- Explore reachability analysis
Command Line Demo #
For a focused algorithms demonstration:
dart run bin/algorithms_demo.dart
This shows practical examples of:
- Package dependency analysis
- Build order optimization
- Component isolation detection
- Shortest path finding
4. Generic Traversal Utilities #
For BFS-style subgraph exploration around nodes within hop limits. Unlike pattern queries that follow specific paths, this explores neighborhoods in all directions using specified edge types.
// Using the same graph setup from section 2...
// Add this to your existing code:
// Explore everything within 2 hops from Alice, expandSubgraph returns SubgraphResult
final aliceSubgraph = expandSubgraph(
graph,
seeds: {'alice'},
edgeTypesRightward: {'WORKS_FOR', 'MANAGES', 'ASSIGNED_TO', 'LEADS'},
forwardHops: 2,
backwardHops: 0,
);
print(aliceSubgraph.nodes); // {alice, engineering, web_app, mobile_app}
print(aliceSubgraph.edges.length); // 4
// Explore everything connected to engineering team, expandSubgraph returns SubgraphResult
final engineeringSubgraph = expandSubgraph(
graph,
seeds: {'engineering'},
edgeTypesRightward: {'ASSIGNED_TO'},
edgeTypesLeftward: {'WORKS_FOR', 'MANAGES'},
forwardHops: 1,
backwardHops: 1,
);
print(engineeringSubgraph.nodes); // {engineering, web_app, mobile_app, alice, bob, charlie}
print(engineeringSubgraph.edges.length); // 5
// Find everyone within 2 hops of projects, expandSubgraph returns SubgraphResult
final projectEcosystem = expandSubgraph(
graph,
seeds: {'web_app', 'mobile_app', 'campaign'},
edgeTypesRightward: {'WORKS_FOR', 'MANAGES'},
edgeTypesLeftward: {'ASSIGNED_TO', 'LEADS'},
forwardHops: 0,
backwardHops: 2,
);
print(projectEcosystem.nodes); // {web_app, mobile_app, campaign, engineering, alice, design, marketing}
print(projectEcosystem.edges.length); // 7
When to use vs Pattern Queries #
Use Case | Pattern Queries | expandSubgraph |
---|---|---|
Specific paths | person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project |
No |
Neighborhood exploration | No | Yes - "Everything around Alice" |
Impact analysis | No | Yes - "What's affected by this change?" |
Subgraph extraction | No | Yes - For visualization/analysis |
Known relationships | Yes - Clear path patterns | No |
Unknown structure | No | Yes - Explore what's connected |
5. Pattern Query Examples #
- Simple patterns:
"user:User"
→{alice, bob, charlie}
- Forward patterns:
"user-[:MEMBER_OF]->group"
- Backward patterns:
"resource<-[:CAN_ACCESS]-group<-[:MEMBER_OF]-user"
→{alice, bob}
- Label filtering:
"user:User{label~Admin}"
→{bob}
- Multiple edge types:
"person-[:WORKS_FOR|VOLUNTEERS_AT]->org"
→ matches ANY of the specified relationship types - Mixed directions:
"person1-[:WORKS_FOR]->team<-[:MANAGES]-manager"
→ finds common connections and shared relationships - Variable-length paths:
"manager-[:MANAGES*1..3]->subordinate"
→ finds direct and indirect reports
6. Mini-Cypher Reference #
GraphKit uses a simplified version of Cypher - the query language used by Neo4j (the most popular graph database). Think of it like SQL for graphs.
What is Cypher? #
Cypher is a language designed to describe patterns in graphs. Instead of writing complex code to traverse relationships, you draw the path with text:
- SQL:
SELECT * FROM users WHERE department = 'engineering'
- Cypher:
person:Person-[:WORKS_FOR]->team:Team{label=Engineering}
What is "Mini-Cypher"? #
GraphKit supports a subset of Cypher - the most useful parts without the complexity:
Supported: Basic patterns, node types, relationships, label filters, variable-length paths, WHERE clauses with logical operators, parentheses Not supported: Aggregations, complex subqueries
This gives you the power of graph queries without learning the full Cypher language.
How to Build Pattern Queries #
Think of pattern queries like giving directions. Instead of "turn left at the store", you're saying "follow this relationship to that type of thing".
Step 1: Start with What You Want to Find #
When you want to find all people in your company, you write:
query.match('person:Person')
This breaks down into:
person
= What you want to call them in your results (like a nickname):
= "that are of type"Person
= The actual type of thing you're looking for
Think of it like: "Find me all things of type Person, and I'll call them 'person' in my results"
Step 2: Connect Things with Arrows #
Now say you want to know "who works where". You connect person to team:
query.match('person:Person-[:WORKS_FOR]->team:Team')
Reading left to right:
person:Person
= "Start with a person"-[
= "who has a connection":WORKS_FOR
= "of type WORKS_FOR"]->
= "that points to"team:Team
= "a team"
Like saying: "Show me people who have a WORKS_FOR arrow pointing to teams"
Step 3: The Arrow Direction Matters! #
Right arrow ->
means "going out from":
person-[:WORKS_FOR]->team // Person points to team (person works FOR the team)
Left arrow <-
means "coming in to":
team<-[:WORKS_FOR]-person // Person points to team (team is worked for BY person)
Same relationship, different starting point!
Step 4: Chain Multiple Steps #
Want to follow a longer path? Just keep adding arrows:
person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project
This means:
- Start with a person
- Follow their WORKS_FOR connection to a team
- Follow that team's ASSIGNED_TO connection to a project
Like following a trail: person → team → project
Step 5: Filter by Name #
Want to find a specific person? Add their name in curly braces:
person:Person{label=Alice Cooper} // Find exactly "Alice Cooper"
person:Person{label~alice} // Find anyone with "alice" in their name
The ~
means "contains" (like a fuzzy search)
Quick Examples to Try #
// Simple: Find all people
query.match('person:Person')
// Returns: {person: {alice, bob, charlie}}
// Connection: Who works where?
query.match('person-[:WORKS_FOR]->team')
// Returns: {person: {alice, bob}, team: {engineering}}
// Chain: Follow a path through the graph
query.match('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project')
// Returns: {person: {alice, bob}, team: {engineering}, project: {web_app, mobile_app}}
// Backwards: What teams work on this project?
query.match('project<-[:ASSIGNED_TO]-team', startId: 'web_app')
// Returns: {project: {web_app}, team: {engineering}}
// Filter: Find specific person
query.match('person:Person{label~Alice}')
// Returns: {person: {alice}}
Remember: The names you pick (person
, team
, etc.) become the keys in your results!
Variable-Length Paths #
Variable-length paths let you find connections across multiple hops without specifying the exact number of steps:
// Use the unified PatternQuery (includes all advanced features)
final query = PatternQuery(graph);
// Find all direct and indirect reports (1-3 management levels)
query.match('manager-[:MANAGES*1..3]->subordinate')
// Find anyone at least 2 levels down the hierarchy
query.match('manager-[:MANAGES*2..]->subordinate')
// Find dependencies up to 4 steps away
query.match('component-[:DEPENDS_ON*..4]->dependency')
// Find all reachable dependencies (unlimited hops)
query.match('component-[:DEPENDS_ON*]->dependency')
Variable-length syntax:
[:TYPE*]
- Unlimited hops[:TYPE*1..3]
- Between 1 and 3 hops[:TYPE*2..]
- 2 or more hops[:TYPE*..4]
- Up to 4 hops[:TYPE*2]
- Exactly 2 hops
Note: Variable-length paths are fully supported in the unified PatternQuery
implementation.
6.1 Advanced WHERE Clauses and Complex Filtering #
For sophisticated filtering beyond basic patterns, GraphKit supports full WHERE clause syntax with logical operators and parentheses.
Complete Cypher Query Language Guide
The comprehensive guide covers:
- Complex logical expressions with parentheses:
(A AND B) OR (C AND D)
- Multiple comparison operators:
>
,<
,>=
,<=
,=
,!=
- Real-world query examples for HR, project management, and organizational analysis
- Property filtering best practices and performance tips
- Error handling and troubleshooting
Quick examples:
// Complex filtering with parentheses
MATCH person:Person WHERE (person.age > 40 AND person.salary > 100000) OR person.department = "Management"
// Multi-hop with filtering
MATCH person:Person-[:WORKS_FOR]->team:Team-[:WORKS_ON]->project:Project WHERE person.salary > 80000 AND project.status = "active"
Try the Interactive WHERE Demo:
cd example
flutter run -t lib/where_demo.dart
The demo includes sample queries, real-time query execution, and a comprehensive dataset for testing complex WHERE clauses.
7. Comparison with Cypher #
Feature | Real Cypher | graph_kit |
---|---|---|
Mixed directions | Yes | Yes |
Variable length paths | Yes | Yes |
Multiple edge types | [:TYPE1|TYPE2] |
Yes |
Multiple patterns | pattern1, pattern2 |
No |
Optional matches | Yes | Via matchMany |
WHERE clauses | Yes | Yes |
Logical operators | Yes | Yes (AND, OR) |
Parentheses | Yes | Yes |
8. Design and performance #
- Traversal from a known ID (
startId
) is fast:- Each hop uses adjacency maps; cost is proportional to the edges visited.
- Seeding by type (
alias:Type
) does a one-time node scan to find initial seeds.- For small/medium graphs, this is effectively instant; indexing can be added later if needed.
matchMany([...])
mirrors "multiple MATCH/OPTIONAL MATCH" lines in Cypher by running several independent chains from the same start and unioning results.
9. JSON Serialization #
Save and load graphs to/from JSON for persistence and data exchange:
import 'dart:io';
import 'package:graph_kit/graph_kit.dart';
// Build your graph
final graph = Graph<Node>();
graph.addNode(Node(id: 'alice', type: 'User', label: 'Alice',
properties: {'email': 'alice@example.com', 'active': true}));
graph.addNode(Node(id: 'team1', type: 'Team', label: 'Engineering'));
graph.addEdge('alice', 'MEMBER_OF', 'team1');
// Serialize to JSON
final json = graph.toJson();
final jsonString = graph.toJsonString(pretty: true);
// Save to file
await File('graph.json').writeAsString(jsonString);
// Load from file
final loadedJson = await File('graph.json').readAsString();
final restoredGraph = GraphSerializer.fromJsonString(loadedJson, Node.fromJson);
// Graph is fully restored - queries work immediately
final query = PatternQuery(restoredGraph);
final members = query.match('team<-[:MEMBER_OF]-user', startId: 'team1');
print(members['user']); // {alice}
10. Examples index #
Dart CLI Examples #
bin/showcase.dart
– comprehensive graph demo with multiple query examplesbin/access_control.dart
– access control patterns with users, groups, and resourcesbin/project_dependencies.dart
– project dependency analysis and traversalbin/social_network.dart
– social network relationships and friend recommendationsbin/serialization_demo.dart
– JSON serialization and persistencebin/algorithms_demo.dart
– graph algorithms demonstration with dependency analysis
Run any example:
dart run bin/showcase.dart
dart run bin/access_control.dart
Flutter Example App #
Interactive graph visualization with pattern queries:
cd example
# Main demo - visual graph with pattern queries
flutter run
# WHERE clause demo - interactive Cypher query testing
flutter run -t lib/where_demo.dart
# RETURN clause demo - property projection and destructuring
flutter run -t lib/return_demo.dart
# Features:
# - Visual graph with nodes and edges
# - Live pattern query execution
# - WHERE clause testing with sample data
# - RETURN clause with before/after comparison
# - Destructuring examples
# - Path highlighting and visualization
# - Example queries with one-click execution
License #
See LICENSE
.