WOQL Tips and Tricks

Open inAnthropic

Introduction

Writing WOQL queries effectively requires understanding not just the language syntax, but also practical patterns for handling real-world scenarios. This guide covers techniques that make your queries more robust, easier to debug, and simpler to maintain.

The easiest first way to think about WOQL is that it is similar to SQL, but for RDF data. In a regular database, you select columns from tables, and you can use the "AS" keyword to rename columns in the result set. The result set in WOQL is similar, but instead of tables, you select variables from triples, and the names of the variables are returned in the result set.

What is unique about WOQL is that it is easy to transform and perform post-processing of the found data using logic. You can use the opt() predicate to make optional bindings, and the comment() predicate to disable code blocks.


Tip 1: Use opt() for Optional Bindings

The Problem

When a WOQL query fails to bind a variable, the entire query fails. This can be frustrating when you want to retrieve data even if some fields are missing or don't match.

Example: JavaScript
// This query fails entirely if ANY person lacks an email
let v = Vars("person", "name", "email");
and(
  triple(v.person, "rdf:type", "@schema:Person"),
  triple(v.person, "name", v.name),
  triple(v.person, "email", v.email)  // Fails if email doesn't exist
)

The Solution

Wrap optional bindings in opt() to make them non-blocking. The query succeeds even when the optional binding fails, returning results with unbound variables where data is missing.

Example: JavaScript
let v = Vars("person", "name", "email");
and(
  triple(v.person, "rdf:type", "@schema:Person"),
  triple(v.person, "name", v.name),
  opt().triple(v.person, "email", v.email)  // Optional: query succeeds even without email
)

When to Use opt()

  • Missing properties: When documents may not have all fields populated
  • Exploratory queries: When discovering what data exists
  • Partial matches: When you want results even if some conditions don't match
  • Schema variations: When querying data that evolved over time with different required fields

Practical Example

Query all employees with their manager, where manager is optional:

Example: JavaScript
let v = Vars("employee", "name", "department", "manager", "manager_name");
and(
  triple(v.employee, "rdf:type", "@schema:Employee"),
  triple(v.employee, "name", v.name),
  triple(v.employee, "department", v.department),
  opt().and(
    triple(v.employee, "reports_to", v.manager),
    triple(v.manager, "name", v.manager_name)
  )
)

This returns all employees, with manager information where it exists.


Tip 2: Use comment() to Disable Code Blocks

The Problem

When debugging complex queries, you often want to temporarily disable parts of the query without deleting them. Traditional approaches like commenting out JSON or JavaScript are error-prone and can break query structure.

The Solution

Use the comment() predicate to wrap query blocks you want to disable. The wrapped code is syntactically valid but not executed.

Example: JavaScript
let v = Vars("person", "name", "age", "city");
and(
  triple(v.person, "rdf:type", "@schema:Person"),
  triple(v.person, "name", v.name),
  
  // Temporarily disabled while debugging
  comment("Disabled age filter for testing").and(
    triple(v.person, "age", v.age),
    greater(v.age, 18)
  ),
  
  triple(v.person, "city", v.city)
)

Benefits

  • Preserves query structure: No syntax errors from incomplete JSON
  • Self-documenting: The comment string explains why it's disabled
  • Easy toggling: Simple to re-enable by removing the comment() wrapper
  • Version control friendly: Changes are clear in diffs

Practical Example: Iterative Query Development

Build complex queries incrementally by commenting out sections:

Example: JavaScript
let v = Vars("order", "customer", "product", "total", "date");
and(
  // Core query - always runs
  triple(v.order, "rdf:type", "@schema:Order"),
  triple(v.order, "customer", v.customer),
  
  // Step 1: Enable to add product details
  comment("Product join - enable when customer query works").and(
    triple(v.order, "line_items", v.item),
    triple(v.item, "product", v.product)
  ),
  
  // Step 2: Enable to add aggregation
  comment("Total calculation - enable after product join works").and(
    triple(v.order, "total", v.total),
    greater(v.total, 100)
  ),
  
  // Step 3: Enable to add date filtering
  comment("Date filter - final step").and(
    triple(v.order, "date", v.date),
    greater(v.date, "2024-01-01")
  )
)

Combining Tips: Robust Query Patterns

Combine opt() and comment() for maximum flexibility during development:

Example: JavaScript
let v = Vars("person", "name", "email", "phone", "address");
and(
  triple(v.person, "rdf:type", "@schema:Person"),
  triple(v.person, "name", v.name),
  
  // Optional fields that may not exist
  opt().triple(v.person, "email", v.email),
  opt().triple(v.person, "phone", v.phone),
  
  // Commented out while testing core query
  comment("Address lookup - re-enable after basic query works").and(
    opt().triple(v.person, "address", v.address)
  )
)

Complete example to show the techniques

This example uses the high performance set_member() predicate to check if a value is in a set. It also uses the opt() predicate to make optional bindings, and the comment() predicate to disable code blocks.

Example: WOQL
// Pick specific variables to materialize in the result (check and result)
select("v:check","v:result").and(
  // Create a set from a list
  eq("v:list", [1, 2, 3, 4]),
  list_to_set("v:list", "v:set"),
  
  // Check if the number 1 is in the set, typecast in different ways
  or(
    and(
      // As expected, the default type is xsd:decimal above, and there is a match
      eq("v:check", "decimal 1 is in the set"),
      opt().and(
        set_member(literal(1, "xsd:decimal"), "v:set"),
        eq("v:result", "consistent"),
      ),
    ),
    and(
      // There is no automatic type conversion, so the string "1" is not in the set
      eq("v:check", "string \"1\" is NOT in the set"),
      opt().and(
        not().set_member(literal("1", "xsd:string"), "v:set"),
        eq("v:result", "consistent"),
      ),
    ),
    and(
      // There is an important distinction between xsd:decimal and xsd:double
      // All number types are distinct in sets, so the double 1 is not in the decimal set 
      eq("v:check", "double 1 is NOT in the decimal set"),
      opt().and(
        // Use a variable to store the member value
        eq("v:member", literal(1, "xsd:double")),
        not().set_member("v:member", "v:set"),
        eq("v:result", "consistent"),

        // We could use a type_of predicate to check the type of the member variable
        //type_of("v:member", "v:member_type"),
      ),
    ),
  ),
  // If any of the solutions do not set the bind result to be consistent,
  // the result is inconsistent, so we can use a late binding of the variable.
  opt().eq("v:result", "inconsistent"),
)

This is the WOQL result, when converting the bindings to a table representation (the result of the query):

CheckResult
decimal 1 is in the setconsistent
string "1" is NOT in the setconsistent
double 1 is NOT in the decimal setconsistent

What we learned:

  • we can use the opt() predicate to make optional bindings
  • the or() predicate can be used to branch out multiple solutions
  • using opt() late binding of variables, we can provide a default value
  • we can use type_of() to check the type of a variable
  • it's easy to test for many solutions using a common core with or()

Summary

TechniqueUse CaseBenefit
opt()Missing or optional dataQuery succeeds with partial results
comment()Debugging and developmentDisable code without breaking syntax

These patterns make WOQL queries more resilient and easier to develop iteratively. Start with a simple query, add complexity gradually using comment() to isolate new sections, and use opt() to handle the inherent variability in real-world data.


Was this helpful?