Skip to content
Open
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
242 changes: 242 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# miniGU User Guide

miniGU is a graph database designed for learning purposes, implemented in Rust. This guide covers the basic usage and GQL query syntax.

## Table of Contents

- [Getting Started](#getting-started)
- [GQL Query Syntax](#gql-query-syntax)
- [MATCH Clause](#match-clause)
- [OPTIONAL MATCH Clause](#optional-match-clause)
- [INSERT Clause](#insert-clause)
- [WHERE Clause](#where-clause)
- [RETURN Clause](#return-clause)
- [Graph Operations](#graph-operations)
- [Examples](#examples)

## Getting Started

Start the interactive shell:

```bash
cargo run -- shell # debug mode
cargo run -r -- shell # release mode
```

## GQL Query Syntax

miniGU supports a subset of the ISO GQL standard.

### MATCH Clause

The `MATCH` clause is used to query graph patterns.

```sql
-- Simple vertex match
MATCH (n:Person) RETURN n;

-- Edge pattern
MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, b, r;

-- Path pattern with variable length
MATCH (n:Person)-[r:KNOWS]->{1,3}(m:Person) RETURN n, m;
```

### OPTIONAL MATCH Clause

`OPTIONAL MATCH` implements LEFT JOIN semantics - all rows from the preceding query are preserved, and NULL values are generated for the optional pattern when no match is found.

#### Syntax

```sql
OPTIONAL MATCH <pattern> [WHERE <condition>] RETURN <variables>
```

#### Semantics

- **LEFT JOIN behavior**: All rows from the previous result are preserved
- **NULL generation**: When the optional pattern doesn't match, NULL values are generated for the pattern's variables
- **Multiple matches**: When multiple matches exist, each match produces a separate result row (cross product)

#### Examples

```sql
-- Basic OPTIONAL MATCH - returns NULL if no match
OPTIONAL MATCH (n:Person) WHERE n.id = 999 RETURN n.name;

-- OPTIONAL MATCH with edge pattern
OPTIONAL MATCH (a:Account)-[e:transfer]->(b:Account)
WHERE e.amount > 100
RETURN a, b, e.amount;

-- Combining MATCH and OPTIONAL MATCH
MATCH (m:Person) WHERE m.id = 274877907096
OPTIONAL MATCH (m)-[e:replyOf]->(c:Comment)
RETURN m, c;
```

#### NULL Handling

When an `OPTIONAL MATCH` doesn't find a match:

- Vertex variables return NULL
- Edge variables return NULL
- Property access on NULL returns NULL
- Use `IS NULL` or `IS NOT NULL` to check for NULL values

```sql
MATCH (n:Person)
OPTIONAL MATCH (n)-[r:KNOWS]->(friend:Person)
RETURN n.name, friend.name IS NULL AS has_no_friends;
```

### INSERT Clause

Insert vertices and edges into the graph.

```sql
-- Insert a vertex
INSERT (n:Person {name: 'Alice', age: 30});

-- Insert an edge
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
INSERT (a)-[r:KNOWS {since: 2020}]->(b);
```

### WHERE Clause

Filter query results with conditions.

```sql
-- Simple condition
MATCH (n:Person) WHERE n.age > 25 RETURN n;

-- Pattern existence
MATCH (p:Person)-[r:IS_FRIENDS_WITH]->(friend:Person)
WHERE EXISTS (MATCH (p)-[:WORKS_FOR]->(:Company {name: "GQL, Inc."}))
RETURN p, r, friend;

-- LIKE pattern matching
MATCH (n:Person) WHERE n.name LIKE '%ob%' RETURN n;
```

### RETURN Clause

Specify what to return from the query.

```sql
-- Return variables
MATCH (n:Person) RETURN n.name, n.age;

-- With aliases
MATCH (n:Person) RETURN n.name AS name, n.age AS age;

-- With aggregation
MATCH (n:Person) RETURN COUNT(n) AS total;
MATCH (n:Person)-[r:KNOWS]->(m:Person) RETURN SUM(m.age), MAX(m.age), MIN(m.age);

-- With ORDER BY and LIMIT
MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC LIMIT 10;
```

## Graph Operations

### Creating a Graph

```sql
-- Create a graph with schema
CREATE GRAPH my_graph {
(Person: PersonLabel {
id STRING,
name STRING,
age INT64,
PRIMARY KEY (id)
}),
(Person)-[KNOWS: KnowsLabel {
since INT64,
PRIMARY KEY (SOURCE_PRIMARY_KEY, since, DESTINATION_PRIMARY_KEY)
}]->(Person)
};

-- Use the graph
USE GRAPH my_graph;
```

### Dropping a Graph

```sql
DROP GRAPH my_graph;
```

### Built-in Procedures

```sql
-- Create a test graph with sample data
CALL create_test_graph('test_graph');

-- Create a test graph with specific number of vertices
CALL create_test_graph_data('my_graph', 10);

-- Show available procedures
CALL show_procedures();

-- Show current graph
CALL show_graph();
```

## Examples

### Social Network Query

```sql
-- Create test data
CALL create_test_graph_data('social', 20);
USE GRAPH social;

-- Find all persons and their friends (including those without friends)
MATCH (p:Person)
OPTIONAL MATCH (p)-[:FRIEND]->(friend:Person)
RETURN p.name, friend.name;

-- Find persons who work at a company, optionally with their friends
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
OPTIONAL MATCH (p)-[f:FRIEND]->(friend:Person)
RETURN p.name, c.name, friend.name;
```

### Financial Transaction Analysis (FinBench)

```sql
-- TSR2: Complex read with multiple OPTIONAL MATCH
MATCH (n:Account{id:12}) RETURN n
NEXT
OPTIONAL MATCH (n)-[e:transfer]->(m:Account)
WHERE e.ts > 45 AND e.ts < 50
RETURN n, SUM(e.amount) AS sumEdge1Amount, MAX(e.amount) AS maxEdge1Amount, COUNT(e) AS numEdge1
NEXT
OPTIONAL MATCH (n)<-[e:transfer]-(m:Account)
WHERE e.ts > 0 AND e.ts < 100
RETURN sumEdge1Amount, maxEdge1Amount, numEdge1, SUM(e.amount) AS sumEdge2Amount;
```

### Path Queries

```sql
-- Find paths of length 1-3
MATCH p=(n:Person)-[r:KNOWS]->{1,3}(m:Person)
RETURN p;

-- With specific length
MATCH (n:Person)-[r:KNOWS]->{2}(m:Person)
RETURN n.name, m.name;

-- Variable length range
MATCH (n:Person)-[r:KNOWS]->{1,5}(m:Person)
RETURN n.name, m.name LIMIT 10;
```

## Notes

- miniGU uses in-memory storage by default
- All queries are case-sensitive for labels and property names
- The `NEXT` keyword separates multiple query statements in a batch
4 changes: 4 additions & 0 deletions minigu-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ path = "src/insta_test.rs"
name = "sqllogictest"
path = "src/sqllogictest.rs"

[[test]]
name = "optional_match_test"
path = "src/optional_match_test.rs"

[[bench]]
harness = false
name = "alloc"
Expand Down
10 changes: 10 additions & 0 deletions minigu-test/gql/misc/optional_match.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Test basic OPTIONAL MATCH
-- This test verifies the OPTIONAL MATCH parsing and planning

-- Basic OPTIONAL MATCH with no matches (should return NULL for optional columns)
OPTIONAL MATCH (n:Person) WHERE n.id = 999 RETURN n.name;

-- OPTIONAL MATCH with pattern
OPTIONAL MATCH (a:Account)-[e:transfer]->(b:Account)
WHERE e.amount > 100
Comment on lines +4 to +9

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .gql test file is not registered in minigu-test/src/insta_test.rs under the misc dataset, so it won’t be executed by the e2e/parse snapshot harness. Also, both statements use WHERE inside MATCH/OPTIONAL MATCH patterns; the optimizer currently rejects graph-pattern predicates (MATCH with predicate (WHERE) is not supported yet). If you register this test, it will likely fail until predicate support is implemented (or the test is rewritten to avoid WHERE for now).

Suggested change
-- Basic OPTIONAL MATCH with no matches (should return NULL for optional columns)
OPTIONAL MATCH (n:Person) WHERE n.id = 999 RETURN n.name;
-- OPTIONAL MATCH with pattern
OPTIONAL MATCH (a:Account)-[e:transfer]->(b:Account)
WHERE e.amount > 100
-- Basic OPTIONAL MATCH on a single node pattern
OPTIONAL MATCH (n:Person) RETURN n.name;
-- OPTIONAL MATCH with relationship pattern
OPTIONAL MATCH (a:Account)-[e:transfer]->(b:Account)

Copilot uses AI. Check for mistakes.
RETURN a, b, e.amount;
2 changes: 2 additions & 0 deletions minigu-test/gql/utility/explain_optional_match.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Explain OPTIONAL MATCH plan
EXPLAIN OPTIONAL MATCH (n:Person) RETURN n;
Comment on lines +1 to +2

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .gql test file is not currently registered in minigu-test/src/insta_test.rs under the utility dataset, so it won’t be executed by the e2e/parse snapshot harness. If the intent is to validate OPTIONAL MATCH plans via EXPLAIN, add it to the add_e2e_tests!("utility", [...]) and add_parser_tests!("utility", [...]) lists (and add/update snapshots).

Copilot uses AI. Check for mistakes.
68 changes: 68 additions & 0 deletions minigu-test/src/optional_match_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Integration test for OPTIONAL MATCH functionality.

use minigu::database::Database;

/// Test that OPTIONAL MATCH can be parsed and planned.
#[test]
fn test_optional_match_parse_and_plan() {
// Create an in-memory database
let db = Database::open_in_memory(Default::default()).expect("Failed to create database");
let mut session = db.session().expect("Failed to create session");

// Parse and plan a simple OPTIONAL MATCH query
let query = "OPTIONAL MATCH (n:Person) RETURN n";

// This should not panic - if it parses and plans successfully, the test passes
let result = session.query(query);

// We expect this to work even though there's no data
// The plan should be generated successfully
match result {
Ok(_) => {
// Query executed successfully (even if no results)
println!("OPTIONAL MATCH query executed successfully");
}
Err(e) => {
// If there's an error, it should be a runtime error, not a parse/plan error
// Parse/plan errors would indicate our implementation is broken
let error_msg = e.to_string();
// Check that it's not a parse error or not implemented error
assert!(
!error_msg.contains("parse error") && !error_msg.contains("not implemented"),
"Query failed with parse/plan error: {}",
error_msg
);
println!(
"Query failed with runtime error (expected for empty database): {}",
error_msg
);
}
}
}

/// Test that OPTIONAL MATCH with pattern can be parsed.
#[test]
fn test_optional_match_with_edge_pattern() {
let db = Database::open_in_memory(Default::default()).expect("Failed to create database");
let mut session = db.session().expect("Failed to create session");

// Parse and plan OPTIONAL MATCH with edge pattern
let query = "OPTIONAL MATCH (a:Account)-[e:transfer]->(b:Account) RETURN a, b, e";

let result = session.query(query);

match result {
Ok(_) => {
println!("OPTIONAL MATCH with edge pattern executed successfully");
}
Err(e) => {
let error_msg = e.to_string();
assert!(
!error_msg.contains("parse error") && !error_msg.contains("not implemented"),
"Query failed with parse/plan error: {}",
error_msg
);
println!("Query failed with runtime error (expected): {}", error_msg);
}
}
}
11 changes: 11 additions & 0 deletions minigu/gql/execution/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::evaluator::vertex_constructor::VertexConstructor;
use crate::executor::create_vector_index::CreateVectorIndexBuilder;
use crate::executor::drop_vector_index::DropVectorIndexBuilder;
use crate::executor::join::JoinCond;
use crate::executor::optional_match::OptionalMatchBuilder;
use crate::executor::procedure_call::ProcedureCallBuilder;
use crate::executor::sort::SortSpec;
use crate::executor::vector_index_scan::VectorIndexScanBuilder;
Expand Down Expand Up @@ -308,6 +309,16 @@ impl ExecutorBuilder {
DropVectorIndexBuilder::new(self.session.clone(), drop_index.clone())
.into_executor()
}
PlanNode::PhysicalOptionalMatch(optional_match) => {
assert_eq!(children.len(), 2);
let left_executor = self.build_executor(&children[0]);
let right_executor = self.build_executor(&children[1]);
let right_schema = optional_match.right_schema.clone();
Box::new(
OptionalMatchBuilder::new(left_executor, right_executor, right_schema)
.into_executor(),
)
}
_ => unreachable!(),
}
}
Expand Down
1 change: 1 addition & 0 deletions minigu/gql/execution/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod factorized_filter;
pub mod filter;
pub mod flatten;
pub mod offset;
pub mod optional_match;
pub mod procedure_call;

// TODO: Implement join executor.
Expand Down
Loading
Loading