Chapter 5: The GQL Query Language

AstraeaDB implements a query language compatible with the ISO GQL standard (released 2024). This chapter covers pattern matching, filtering, mutations, aggregation, and built-in functions—everything you need to read and write data using declarative queries.

Prerequisites This chapter assumes you have a running AstraeaDB server and a connected client as described in Chapter 3 and Chapter 4. All examples use the social-network graph built in Chapter 4.

The Python Query Interface

The primary way to execute GQL in AstraeaDB is through the query() method available on every client library. The Python API looks like this:

result = client.query("GQL STRING")

Every query returns a dictionary with three keys:

{
    "columns": ["a.name", "b.name"],    // column headers from RETURN
    "rows":    [["Alice", "Bob"], ...], // result rows
    "stats":   {"nodes_created": 0, ...} // mutation statistics
}

5.1 Pattern Matching Deep Dive

Pattern matching is the heart of GQL. Instead of writing procedural code to walk a graph, you describe the shape of the subgraph you want, and the engine finds all matches.

Node Patterns

A node is represented by parentheses. You can optionally bind it to a variable, add a label, or specify properties:

PatternMeaning
(n)Any node, bound to variable n
(n:Person)Any node with the label Person
(n:Person {name: "Alice"})A Person node whose name property equals "Alice"
(:Movie)An anonymous Movie node (no variable binding)

Edge Patterns

Edges connect nodes with arrows. The arrow direction indicates relationship direction:

PatternMeaning
-[:KNOWS]->Outgoing edge of type KNOWS
<-[:KNOWS]-Incoming edge of type KNOWS
-[:KNOWS]-Undirected (either direction)
-[e:KNOWS]->Outgoing KNOWS edge bound to variable e
-[e:KNOWS {since: 2020}]->Edge with a property constraint

Combined Patterns

Combine node and edge patterns into full graph shapes:

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name

This finds every pair of Person nodes connected by a KNOWS edge and returns both names.

Multi-Hop Patterns

Chain patterns to traverse multiple hops in a single query:

-- Find friends-of-friends
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)
RETURN a.name, c.name

Visual Pattern Matching

One of the most intuitive aspects of GQL is that the query looks like the shape it matches. Consider this ASCII representation of a movie database:

Query pattern: (a:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person) Matches this subgraph shape: [Alice] [Charlie] :Person :Person \ / :ACTED_IN :DIRECTED \ / v v [The Matrix] :Movie Every triplet of (actor, movie, director) in the database that matches this shape will appear in the results.

Complete Example with Tabs

from astraeadb import AstraeaClient

with AstraeaClient() as client:
    # Find all actors and the movies they acted in
    result = client.query("""
        MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
        RETURN a.name, m.title
    """)

    for row in result["rows"]:
        print(f"{row[0]} acted in {row[1]}")

    # Multi-hop: friends of friends
    fof = client.query("""
        MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person)
        RETURN a.name AS person, c.name AS friend_of_friend
    """)

    print(fof["columns"])  # ["person", "friend_of_friend"]
    print(fof["rows"])     # [["Alice", "Charlie"], ...]
library(astraeadb)

client <- astraea_client()
client$connect()

# Find all actors and the movies they acted in
result <- client$query("
    MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
    RETURN a.name, m.title
")

for (i in seq_len(nrow(result$rows))) {
    cat(result$rows[i, 1], "acted in", result$rows[i, 2], "\n")
}

# Multi-hop: friends of friends
fof <- client$query("
    MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person)
    RETURN a.name AS person, c.name AS friend_of_friend
")

print(fof$columns)  # [1] "person" "friend_of_friend"
print(fof$rows)     # data.frame with results

client$close()
package main

import (
    "context"
    "fmt"
    "github.com/AstraeaDB/AstraeaDB-Official"
)

func main() {
    ctx := context.Background()
    client := astraeadb.NewClient(
        astraeadb.WithAddress("127.0.0.1", 7687),
    )
    client.Connect(ctx)
    defer client.Close()

    // Find all actors and the movies they acted in
    result, _ := client.Query(ctx, `
        MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
        RETURN a.name, m.title
    `)

    for _, row := range result.Rows {
        fmt.Printf("%s acted in %s\n", row[0], row[1])
    }

    // Multi-hop: friends of friends
    fof, _ := client.Query(ctx, `
        MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person)
        RETURN a.name AS person, c.name AS friend_of_friend
    `)

    fmt.Println(fof.Columns) // [person friend_of_friend]
    fmt.Println(fof.Rows)    // [[Alice Charlie] ...]
}
import com.astraeadb.unified.UnifiedClient;
import com.astraeadb.unified.QueryResult;

public class PatternMatchExample {
    public static void main(String[] args) {
        try (var client = UnifiedClient.builder()
                .host("127.0.0.1").build()) {
            client.connect();

            // Find all actors and the movies they acted in
            QueryResult result = client.query("""
                MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
                RETURN a.name, m.title
                """);

            for (var row : result.rows()) {
                System.out.printf("%s acted in %s%n",
                    row.get(0), row.get(1));
            }

            // Multi-hop: friends of friends
            QueryResult fof = client.query("""
                MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person)
                RETURN a.name AS person, c.name AS friend_of_friend
                """);

            System.out.println(fof.columns());
            System.out.println(fof.rows());
        }
    }
}

5.2 Filtering with WHERE

The WHERE clause filters matched patterns based on property values. It appears after MATCH and before RETURN.

Comparison Operators

OperatorMeaningExample
=Equal toWHERE n.age = 30
<>Not equal toWHERE n.name <> "Bob"
<Less thanWHERE n.age < 25
>Greater thanWHERE n.age > 25
<=Less than or equalWHERE n.age <= 30
>=Greater than or equalWHERE n.age >= 18

Boolean Operators

Combine conditions with AND, OR, and NOT:

-- Multiple conditions with AND
MATCH (n:Person)
WHERE n.age > 25 AND n.name <> "Bob"
RETURN n.name, n.age
-- OR to broaden the filter
MATCH (n:Person)
WHERE n.city = "London" OR n.city = "Paris"
RETURN n.name, n.city
-- NOT to negate a condition
MATCH (n:Person)
WHERE NOT n.age < 18
RETURN n.name, n.age

String Matching

Use the STARTS WITH, ENDS WITH, and CONTAINS operators for string filtering:

-- Find people whose name starts with "A"
MATCH (n:Person)
WHERE n.name STARTS WITH "A"
RETURN n.name
-- Find movies containing "Matrix" in the title
MATCH (m:Movie)
WHERE m.title CONTAINS "Matrix"
RETURN m.title, m.year

Complete Filtering Example

with AstraeaClient() as client:
    # Find adults in London who know someone over 30
    result = client.query("""
        MATCH (a:Person)-[:KNOWS]->(b:Person)
        WHERE a.city = "London"
          AND a.age >= 18
          AND b.age > 30
        RETURN a.name, a.age, b.name, b.age
    """)

    for row in result["rows"]:
        print(f"{row[0]} (age {row[1]}) knows {row[2]} (age {row[3]})")

# Example output:
# columns: ["a.name", "a.age", "b.name", "b.age"]
# rows:    [["Alice", 30, "Charlie", 35], ...]
client <- astraea_client()
client$connect()

# Find adults in London who know someone over 30
result <- client$query('
    MATCH (a:Person)-[:KNOWS]->(b:Person)
    WHERE a.city = "London"
      AND a.age >= 18
      AND b.age > 30
    RETURN a.name, a.age, b.name, b.age
')

print(result$rows)
#   a.name a.age  b.name b.age
# 1  Alice    30 Charlie    35

client$close()
ctx := context.Background()
client := astraeadb.NewClient(
    astraeadb.WithAddress("127.0.0.1", 7687),
)
client.Connect(ctx)
defer client.Close()

// Find adults in London who know someone over 30
result, _ := client.Query(ctx, `
    MATCH (a:Person)-[:KNOWS]->(b:Person)
    WHERE a.city = "London"
      AND a.age >= 18
      AND b.age > 30
    RETURN a.name, a.age, b.name, b.age
`)

for _, row := range result.Rows {
    fmt.Printf("%s (age %v) knows %s (age %v)\n",
        row[0], row[1], row[2], row[3])
}
try (var client = UnifiedClient.builder()
        .host("127.0.0.1").build()) {
    client.connect();

    // Find adults in London who know someone over 30
    QueryResult result = client.query("""
        MATCH (a:Person)-[:KNOWS]->(b:Person)
        WHERE a.city = "London"
          AND a.age >= 18
          AND b.age > 30
        RETURN a.name, a.age, b.name, b.age
        """);

    for (var row : result.rows()) {
        System.out.printf("%s (age %s) knows %s (age %s)%n",
            row.get(0), row.get(1),
            row.get(2), row.get(3));
    }
}

5.3 RETURN, ORDER BY, SKIP, LIMIT

The RETURN clause defines which columns appear in the result set. Combined with ORDER BY, SKIP, and LIMIT, it gives you full control over result shape and pagination.

Projections

Return specific properties rather than entire nodes:

MATCH (n:Person)
RETURN n.name, n.age

Result:

columns: ["n.name", "n.age"]
rows:
  ["Alice",   30]
  ["Bob",     28]
  ["Charlie", 35]
  ["Diana",   22]

Aliasing with AS

Rename columns in the output for clarity:

MATCH (n:Person)
RETURN n.name AS person_name, n.age AS person_age

Result:

columns: ["person_name", "person_age"]
rows:
  ["Alice",   30]
  ["Bob",     28]
  ...

Sorting with ORDER BY

Sort results by one or more expressions. Use ASC (default) or DESC:

MATCH (n:Person)
RETURN n.name, n.age
ORDER BY n.age DESC

Result:

rows:
  ["Charlie", 35]
  ["Alice",   30]
  ["Bob",     28]
  ["Diana",   22]

Pagination with SKIP and LIMIT

SKIP ignores the first N rows; LIMIT caps the total rows returned:

-- Page 1: first 2 results
MATCH (n:Person)
RETURN n.name, n.age
ORDER BY n.age DESC
LIMIT 2
rows: [["Charlie", 35], ["Alice", 30]]
-- Page 2: skip first 2, take next 2
MATCH (n:Person)
RETURN n.name, n.age
ORDER BY n.age DESC
SKIP 2 LIMIT 2
rows: [["Bob", 28], ["Diana", 22]]

Combined Example

with AstraeaClient() as client:
    # Paginated, sorted query
    page_size = 5
    page_num  = 3   # 0-indexed

    result = client.query(f"""
        MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
        RETURN p.name AS actor, m.title AS movie, m.year AS year
        ORDER BY m.year DESC
        SKIP {page_num * page_size}
        LIMIT {page_size}
    """)

    print(result["columns"])  # ["actor", "movie", "year"]
    for row in result["rows"]:
        print(f"  {row[0]:15} | {row[1]:25} | {row[2]}")
page_size <- 5
page_num  <- 3

result <- client$query(sprintf("
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN p.name AS actor, m.title AS movie, m.year AS year
    ORDER BY m.year DESC
    SKIP %d LIMIT %d
", page_num * page_size, page_size))

print(result$rows)
#   actor              movie                    year
# 1 Alice              The Matrix               1999
pageSize := 5
pageNum  := 3

query := fmt.Sprintf(`
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN p.name AS actor, m.title AS movie, m.year AS year
    ORDER BY m.year DESC
    SKIP %d LIMIT %d
`, pageNum*pageSize, pageSize)

result, _ := client.Query(ctx, query)

fmt.Println(result.Columns) // [actor movie year]
for _, row := range result.Rows {
    fmt.Printf("  %-15s | %-25s | %v\n", row[0], row[1], row[2])
}
int pageSize = 5;
int pageNum  = 3;

QueryResult result = client.query(String.format("""
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN p.name AS actor, m.title AS movie, m.year AS year
    ORDER BY m.year DESC
    SKIP %d LIMIT %d
    """, pageNum * pageSize, pageSize));

System.out.println(result.columns());
for (var row : result.rows()) {
    System.out.printf("  %-15s | %-25s | %s%n",
        row.get(0), row.get(1), row.get(2));
}

5.4 Aggregation Functions

AstraeaDB supports standard aggregation functions. When you use an aggregation function in RETURN, all non-aggregated columns act as implicit GROUP BY keys—just like in SQL.

Available Functions

FunctionDescriptionExample
count(n)Number of matched itemsRETURN count(n)
sum(n.age)Sum of numeric valuesRETURN sum(n.age)
avg(n.age)Average of numeric valuesRETURN avg(n.age)
min(n.age)Minimum valueRETURN min(n.age)
max(n.age)Maximum valueRETURN max(n.age)

Implicit GROUP BY

When a RETURN clause mixes aggregated and non-aggregated expressions, the non-aggregated expressions automatically become grouping keys. For example:

-- Count how many actors appeared in each movie
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN m.title, count(p) AS actor_count
ORDER BY count(p) DESC

Result:

columns: ["m.title", "actor_count"]
rows:
  ["The Matrix",      4]
  ["John Wick",       3]
  ["Speed",           2]
  ["Point Break",     2]

More Aggregation Examples

-- Average age of people in each city
MATCH (n:Person)
RETURN n.city, avg(n.age) AS avg_age, count(n) AS population
ORDER BY avg_age DESC
-- Find the youngest and oldest person overall
MATCH (n:Person)
RETURN min(n.age) AS youngest, max(n.age) AS oldest, sum(n.age) AS total_age

Aggregation with Filtering

with AstraeaClient() as client:
    # Movies with at least 3 actors, sorted by cast size
    result = client.query("""
        MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
        RETURN m.title AS movie,
               count(p) AS cast_size,
               avg(p.age) AS avg_age
        ORDER BY cast_size DESC
    """)

    print(f"{'Movie':25} {'Cast':6} {'Avg Age'}")
    print("-" * 42)
    for row in result["rows"]:
        print(f"{row[0]:25} {row[1]:6} {row[2]:.1f}")

# Output:
# Movie                     Cast   Avg Age
# ------------------------------------------
# The Matrix                4      32.5
# John Wick                 3      38.0
result <- client$query("
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN m.title AS movie,
           count(p) AS cast_size,
           avg(p.age) AS avg_age
    ORDER BY cast_size DESC
")

print(result$rows)
#        movie cast_size avg_age
# 1 The Matrix         4    32.5
# 2  John Wick         3    38.0
result, _ := client.Query(ctx, `
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN m.title AS movie,
           count(p) AS cast_size,
           avg(p.age) AS avg_age
    ORDER BY cast_size DESC
`)

fmt.Printf("%-25s %-6s %s\n", "Movie", "Cast", "Avg Age")
for _, row := range result.Rows {
    fmt.Printf("%-25s %-6v %v\n", row[0], row[1], row[2])
}
QueryResult result = client.query("""
    MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
    RETURN m.title AS movie,
           count(p) AS cast_size,
           avg(p.age) AS avg_age
    ORDER BY cast_size DESC
    """);

System.out.printf("%-25s %-6s %s%n", "Movie", "Cast", "Avg Age");
for (var row : result.rows()) {
    System.out.printf("%-25s %-6s %s%n",
        row.get(0), row.get(1), row.get(2));
}

5.5 CREATE and DELETE in GQL

GQL is not just for querying—it also supports data mutation. AstraeaDB's CREATE and DELETE clauses let you build and tear down graph structures declaratively.

Creating Nodes

Create a new node with labels and properties:

CREATE (n:Person {name: "Dave", age: 35, city: "Berlin"})

The stats field in the response confirms the mutation:

{
    "columns": [],
    "rows": [],
    "stats": {"nodes_created": 1, "edges_created": 0,
              "nodes_deleted": 0, "edges_deleted": 0}
}

Creating Edges

Important: MATCH before CREATE for edges In the current AstraeaDB implementation, creating an edge requires that both endpoints already exist. Use MATCH to find the existing nodes first, then CREATE the relationship between them.
-- First, match both endpoints
MATCH (a:Person {name: "Alice"}), (d:Person {name: "Dave"})
CREATE (a)-[:KNOWS {since: 2020}]->(d)

Response stats:

{"stats": {"nodes_created": 0, "edges_created": 1, "nodes_deleted": 0, "edges_deleted": 0}}

Deleting Nodes and Edges

Use MATCH to find the element, then DELETE to remove it:

-- Delete a specific person
MATCH (n:Person {name: "Dave"})
DELETE n
-- Delete a specific edge
MATCH (a:Person {name: "Alice"})-[e:KNOWS]->(b:Person {name: "Bob"})
DELETE e

Response stats for node deletion:

{"stats": {"nodes_created": 0, "edges_created": 0, "nodes_deleted": 1, "edges_deleted": 0}}

Complete CREATE/DELETE Example

with AstraeaClient() as client:
    # Create a new person
    r1 = client.query('CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})')
    print(r1["stats"])  # {"nodes_created": 1, ...}

    # Create an edge between existing nodes
    r2 = client.query("""
        MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"})
        CREATE (a)-[:KNOWS {since: 2024}]->(e)
    """)
    print(r2["stats"])  # {"edges_created": 1, ...}

    # Verify the new connection
    r3 = client.query("""
        MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b)
        RETURN b.name, b.city
    """)
    print(r3["rows"])
    # [["Bob", "London"], ["Eve", "Tokyo"], ...]

    # Delete the person (clean up)
    r4 = client.query('MATCH (n:Person {name: "Eve"}) DELETE n')
    print(r4["stats"])  # {"nodes_deleted": 1, ...}
# Create a new person
r1 <- client$query('CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})')
print(r1$stats)  # nodes_created: 1

# Create an edge between existing nodes
r2 <- client$query('
    MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"})
    CREATE (a)-[:KNOWS {since: 2024}]->(e)
')
print(r2$stats)  # edges_created: 1

# Verify the new connection
r3 <- client$query('
    MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b)
    RETURN b.name, b.city
')
print(r3$rows)

# Delete the person
r4 <- client$query('MATCH (n:Person {name: "Eve"}) DELETE n')
print(r4$stats)  # nodes_deleted: 1
// Create a new person
r1, _ := client.Query(ctx,
    `CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})`)
fmt.Println(r1.Stats) // {NodesCreated:1 ...}

// Create an edge between existing nodes
r2, _ := client.Query(ctx, `
    MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"})
    CREATE (a)-[:KNOWS {since: 2024}]->(e)
`)
fmt.Println(r2.Stats) // {EdgesCreated:1 ...}

// Verify the new connection
r3, _ := client.Query(ctx, `
    MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b)
    RETURN b.name, b.city
`)
fmt.Println(r3.Rows)

// Delete the person
r4, _ := client.Query(ctx,
    `MATCH (n:Person {name: "Eve"}) DELETE n`)
fmt.Println(r4.Stats) // {NodesDeleted:1 ...}
// Create a new person
QueryResult r1 = client.query(
    "CREATE (n:Person {name: \"Eve\", age: 27, city: \"Tokyo\"})");
System.out.println(r1.stats()); // {nodes_created=1, ...}

// Create an edge between existing nodes
QueryResult r2 = client.query("""
    MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"})
    CREATE (a)-[:KNOWS {since: 2024}]->(e)
    """);
System.out.println(r2.stats()); // {edges_created=1, ...}

// Verify the new connection
QueryResult r3 = client.query("""
    MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b)
    RETURN b.name, b.city
    """);
System.out.println(r3.rows());

// Delete the person
QueryResult r4 = client.query(
    "MATCH (n:Person {name: \"Eve\"}) DELETE n");
System.out.println(r4.stats()); // {nodes_deleted=1, ...}

5.6 Built-in Functions

AstraeaDB provides several built-in functions for inspecting and transforming graph elements within GQL queries.

Identity and Metadata Functions

FunctionDescriptionReturns
id(n) Internal numeric identifier for a node or edge Integer
labels(n) List of labels attached to a node List of strings
type(e) The relationship type of an edge String

Examples: id(), labels(), type()

-- Get the internal ID and labels of all Person nodes
MATCH (n:Person)
RETURN id(n), labels(n), n.name
columns: ["id(n)", "labels(n)", "n.name"]
rows:
  [0,  ["Person"],          "Alice"]
  [1,  ["Person"],          "Bob"]
  [4,  ["Person", "Actor"], "Charlie"]
-- Inspect edge types in a pattern
MATCH (a:Person)-[e]->(b)
RETURN a.name, type(e), b.name
columns: ["a.name", "type(e)", "b.name"]
rows:
  ["Alice",   "KNOWS",     "Bob"]
  ["Alice",   "ACTED_IN",  "The Matrix"]
  ["Bob",     "KNOWS",     "Charlie"]

Type Conversion Functions

FunctionDescriptionExample
toString(value) Converts a value to its string representation toString(42) returns "42"
toInteger(value) Converts a string to an integer toInteger("42") returns 42
-- Convert age to string for concatenation
MATCH (n:Person)
RETURN n.name, toString(n.age) AS age_str

The DISTINCT Keyword

Use DISTINCT to eliminate duplicate rows from results:

-- Find unique cities where people live
MATCH (n:Person)
RETURN DISTINCT n.city
columns: ["n.city"]
rows:
  ["London"]
  ["Paris"]
  ["Berlin"]
  ["Tokyo"]
-- Find unique relationship types in the graph
MATCH ()-[e]->()
RETURN DISTINCT type(e) AS relationship_type
columns: ["relationship_type"]
rows:
  ["KNOWS"]
  ["ACTED_IN"]
  ["DIRECTED"]

Built-in Functions Example

with AstraeaClient() as client:
    # Explore the graph metadata
    result = client.query("""
        MATCH (a:Person)-[e]->(b)
        RETURN id(a) AS from_id,
               labels(a) AS from_labels,
               type(e) AS rel_type,
               id(b) AS to_id,
               labels(b) AS to_labels
    """)

    for row in result["rows"]:
        print(f"Node {row[0]} {row[1]} --[{row[2]}]--> Node {row[3]} {row[4]}")

    # Find unique relationship types
    types = client.query("""
        MATCH ()-[e]->()
        RETURN DISTINCT type(e) AS rel_type
    """)
    print("Relationship types:", [r[0] for r in types["rows"]])
    # Relationship types: ["KNOWS", "ACTED_IN", "DIRECTED"]
# Explore the graph metadata
result <- client$query("
    MATCH (a:Person)-[e]->(b)
    RETURN id(a) AS from_id,
           labels(a) AS from_labels,
           type(e) AS rel_type,
           id(b) AS to_id,
           labels(b) AS to_labels
")
print(result$rows)

# Find unique relationship types
types <- client$query("
    MATCH ()-[e]->()
    RETURN DISTINCT type(e) AS rel_type
")
print(types$rows)
#   rel_type
# 1    KNOWS
# 2 ACTED_IN
# 3 DIRECTED
// Explore the graph metadata
result, _ := client.Query(ctx, `
    MATCH (a:Person)-[e]->(b)
    RETURN id(a) AS from_id,
           labels(a) AS from_labels,
           type(e) AS rel_type,
           id(b) AS to_id,
           labels(b) AS to_labels
`)

for _, row := range result.Rows {
    fmt.Printf("Node %v %v --[%v]--> Node %v %v\n",
        row[0], row[1], row[2], row[3], row[4])
}

// Find unique relationship types
types, _ := client.Query(ctx, `
    MATCH ()-[e]->()
    RETURN DISTINCT type(e) AS rel_type
`)
fmt.Println("Relationship types:", types.Rows)
// Explore the graph metadata
QueryResult result = client.query("""
    MATCH (a:Person)-[e]->(b)
    RETURN id(a) AS from_id,
           labels(a) AS from_labels,
           type(e) AS rel_type,
           id(b) AS to_id,
           labels(b) AS to_labels
    """);

for (var row : result.rows()) {
    System.out.printf("Node %s %s --[%s]--> Node %s %s%n",
        row.get(0), row.get(1), row.get(2),
        row.get(3), row.get(4));
}

// Find unique relationship types
QueryResult types = client.query("""
    MATCH ()-[e]->()
    RETURN DISTINCT type(e) AS rel_type
    """);
System.out.println("Relationship types: " + types.rows());

Chapter Summary

Key Takeaways

In the next chapter, we move beyond declarative queries to explore the traversal algorithms built into AstraeaDB: BFS, DFS, shortest path, and neighbor expansion.

← Chapter 4: Your First Graph Chapter 6: Graph Traversals →