Skip to content

Functions

Functions are the foundation of scaf tests. Each function definition declares a named database query that tests will exercise.

fn FunctionName(params) `
-- Your database query here
`

The query body is enclosed in backticks (`) and contains raw database syntax in your configured dialect.

Function names must be valid identifiers:

  • Start with a letter or underscore
  • Contain letters, digits, or underscores
  • Are case-sensitive
fn GetUser(id) `...` // Valid
fn getUserById(id) `...` // Valid
fn get_user_2(id) `...` // Valid
fn _internal(id) `...` // Valid (starts with underscore)

Functions use $paramName syntax for parameters in the query body:

fn GetUser(userId) `
MATCH (u:User {id: $userId})
RETURN u.name, u.email
`
fn SearchUsers(searchTerm, minAge) `
MATCH (u:User)
WHERE u.name CONTAINS $searchTerm
AND u.age >= $minAge
RETURN u
`

Parameters are provided in test cases:

GetUser {
test "finds user" {
$userId: 1
$searchTerm: "ali"
$minAge: 18
u.name: "Alice"
}
}

Parameters can have optional type annotations:

fn GetUser(userId: string) `
MATCH (u:User {id: $userId})
RETURN u.name, u.age
`
TypeDescription
stringText values
intInteger numbers
float64Floating-point numbers
boolBoolean values
anyAny type (default)
fn CreateUser(
id: string,
name: string,
age: int,
score: float64,
verified: bool,
) `
CREATE (u:User {id: $id, name: $name, age: $age, score: $score, verified: $verified})
RETURN u
`

Use ? suffix for nullable types:

fn FindUser(name: string, age: int?) `
MATCH (u:User {name: $name})
WHERE u.age = $age OR $age IS NULL
RETURN u
`

Use [type] for array types:

fn GetUsersByIds(ids: [string]) `
MATCH (u:User) WHERE u.id IN $ids
RETURN u.name
`

Use {keyType: valueType} for map types:

fn CreateWithMeta(data: {string: any}) `
CREATE (n:Node $data)
RETURN n
`

Parameters can span multiple lines with trailing commas:

fn CreateUser(
id: string,
name: string,
email: string,
age: int?, // trailing comma OK
) `
CREATE (u:User {id: $id, name: $name, email: $email, age: $age})
RETURN u
`

Functions with no parameters use empty parentheses:

fn CountAllNodes() `
MATCH (n) RETURN count(n) as total
`

Functions should return named values that tests can reference:

fn GetUserStats(userId) `
MATCH (u:User {id: $userId})
OPTIONAL MATCH (u)-[:AUTHORED]->(p:Post)
RETURN
u.name as name,
u.email as email,
count(p) as postCount,
collect(p.title) as postTitles
`

In tests, reference these by their alias:

GetUserStats {
test "user with posts" {
$userId: 1
name: "Alice"
postCount: 5
postTitles: ["Post 1", "Post 2", "Post 3", "Post 4", "Post 5"]
}
}

For node/relationship returns, use dot notation:

fn GetUserNode(userId) `
MATCH (u:User {id: $userId})
RETURN u
`
GetUserNode {
test "returns user node" {
$userId: 1
u.name: "Alice"
u.email: "alice@example.com"
u.age: 30
}
}

Functions can contain multiple statements:

fn CreateAndReturn(id, name) `
CREATE (u:User {id: $id, name: $name})
WITH u
MATCH (u)-[:FRIENDS_WITH]->(f:User)
RETURN u.name, collect(f.name) as friends
`
fn GetUser(userId) `
MATCH (u:User {id: $userId})
RETURN u.name, u.email
`
fn GetUserWithFriends(userId) `
MATCH (u:User {id: $userId})-[:FRIENDS_WITH]->(f:User)
RETURN u.name, collect(f.name) as friends
`
fn GetUserStats(userId) `
MATCH (u:User {id: $userId})
OPTIONAL MATCH (u)-[:AUTHORED]->(p:Post)
OPTIONAL MATCH (p)<-[:LIKES]-(liker:User)
RETURN
u.name,
count(DISTINCT p) as postCount,
count(liker) as totalLikes
`
fn GetUserOptional(userId) `
MATCH (u:User {id: $userId})
OPTIONAL MATCH (u)-[:HAS_PROFILE]->(p:Profile)
RETURN u.name, p.bio
`
fn CreateUser(id, name, email) `
CREATE (u:User {id: $id, name: $name, email: $email})
RETURN u.id, u.name
`
CreateUser {
test "creates user" {
$id: 999
$name: "New User"
$email: "new@example.com"
u.id: 999
u.name: "New User"
}
}
fn UpdateUserEmail(userId, newEmail) `
MATCH (u:User {id: $userId})
SET u.email = $newEmail
RETURN u.email
`
fn DeleteUser(userId) `
MATCH (u:User {id: $userId})
DELETE u
RETURN count(*) as deleted
`
DeleteUser {
test "deletes user" {
$userId: 1
deleted: 1
}
}
fn GetUser(userId) `
SELECT name, email, age
FROM users
WHERE id = $userId
`
fn GetUserWithPosts(userId) `
SELECT
u.name,
u.email,
COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
WHERE u.id = $userId
GROUP BY u.id, u.name, u.email
`
  1. Use descriptive namesGetUserById is clearer than Q1

  2. Return named values — Makes test assertions more readable

  3. Keep functions focused — One function per operation type

  4. Use parameters — Never hardcode values that should vary

  5. Add type annotations — Improves documentation and validation

  6. Format for readability — Multi-line queries are fine

// Good: Clear, parameterized, typed, returns named values
fn GetActiveUsersInCity(city: string, limit: int) `
MATCH (u:User)
WHERE u.active = true
AND u.city = $city
RETURN
u.name as name,
u.email as email,
u.joinDate as joinedAt
ORDER BY u.joinDate DESC
LIMIT $limit
`
// Avoid: Unclear name, no parameters, no types
fn Q3() `
MATCH (u:User {active: true, city: "NYC"})
RETURN u
`