Back to Blog

OQL: Our Ontology Query Language (Syntax)

Tags: #OQL #QueryLanguage #BNF #GraphTraversal #MetricExpansion #coomia-dip

CoomiaPublished on July 15, 202514 min read
Share this articleTwitter / X

Series: S3 Data Foundation · Article 6 | Level: Advanced | Reading Time: 20 min

OQL: Our Ontology Query Language (Syntax)

Tags: #OQL #QueryLanguage #BNF #GraphTraversal #MetricExpansion #coomia-dip

#TL;DR

The coomia-dip platform features a purpose-built query language for the three-table model — OQL (Ontology Query Language). Rather than a thin SQL wrapper, OQL is a domain-specific language designed from Ontology semantics up, fusing entity queries, graph traversal, metric expansion, and time travel into a cohesive syntax. This article presents the complete BNF grammar, the four core statements (FETCH / TRAVERSE / AGGREGATE / TIMELINE), the metric expansion mechanism, and graph traversal syntax. Through comparisons with SQL, GraphQL, and Cypher, we demonstrate OQL's unique advantages in Ontology scenarios.

#1. Why a Dedicated Query Language

#1.1 SQL's Limitations

Code
Pain points of querying Ontology data with SQL:

Problem 1: Type-agnostic queries need verbose JSON functions
  SQL:   SELECT JSON_EXTRACT(properties, '$.name') FROM entity_common
         WHERE entity_type = 'Person'
         AND JSON_EXTRACT(properties, '$.age') > 30
  OQL:   FETCH Person WHERE age > 30

Problem 2: Graph traversal requires multi-layer JOINs
  SQL:   SELECT ... FROM entity_common ec1
         JOIN entity_edge ee1 ON ...
         JOIN entity_common ec2 ON ...
         JOIN entity_edge ee2 ON ...
         JOIN entity_common ec3 ON ...  -- 3 hops = 5 JOINs
  OQL:   TRAVERSE Person -> WorksAt -> Company -> LocatedIn -> City

Problem 3: Metrics need nested subqueries
  SQL:   SELECT *, (SELECT COUNT(*) FROM entity_edge
         WHERE source_id = ec.entity_id AND edge_type = 'Manages')
         AS direct_reports FROM entity_common ec ...
  OQL:   FETCH Person WITH METRIC direct_reports

Problem 4: Time travel requires versioned JOINs
  SQL:   Extremely complex Iceberg time-travel syntax
  OQL:   FETCH Person AT TIME '2024-01-01T00:00:00Z'

#1.2 OQL Design Goals

Code
OQL Design Goal Matrix:

┌─────────────────────┬──────────────────────────────────┐
│ Goal                 │ Implementation                    │
├─────────────────────┼──────────────────────────────────┤
│ Ontology-native      │ Entity Type as query entry point  │
│                      │ Direct property refs, no JSON fn  │
├─────────────────────┼──────────────────────────────────┤
│ Built-in traversal   │ TRAVERSE statement + path exprs   │
│                      │ Supports 1-N hop traversal        │
├─────────────────────┼──────────────────────────────────┤
│ Metric expansion     │ WITH METRIC clause auto-expands   │
│                      │ Metric defs decoupled from query  │
├─────────────────────┼──────────────────────────────────┤
│ Time travel          │ AT TIME / AT BRANCH clauses       │
│                      │ Transparently maps to Iceberg     │
├─────────────────────┼──────────────────────────────────┤
│ Translatable to SQL  │ Compiles to Doris SQL for exec    │
│                      │ Retains all OLAP optimizations    │
└─────────────────────┴──────────────────────────────────┘

#2. BNF Grammar Definition

#2.1 Top-Level Grammar

BNF
(* OQL Top-Level Grammar - EBNF Notation *)

oql_statement
    ::= fetch_statement
      | traverse_statement
      | aggregate_statement
      | timeline_statement
      | diff_statement
      | mutate_statement
      ;

(* === FETCH Statement: Entity Queries === *)
fetch_statement
    ::= 'FETCH' entity_type_ref
        [ 'WHERE' predicate_expr ]
        [ 'WITH' with_clause ( ',' with_clause )* ]
        [ 'AT' temporal_clause ]
        [ 'IN' world_ref ]
        [ 'ORDER' 'BY' order_expr ( ',' order_expr )* ]
        [ 'LIMIT' integer [ 'OFFSET' integer ] ]
    ;

(* === TRAVERSE Statement: Graph Traversal === *)
traverse_statement
    ::= 'TRAVERSE' entity_ref
        traverse_path+
        [ 'WHERE' predicate_expr ]
        [ 'WITH' with_clause ( ',' with_clause )* ]
        [ 'DEPTH' integer [ '..' integer ] ]
        [ 'LIMIT' integer ]
    ;

(* === AGGREGATE Statement: Analytics === *)
aggregate_statement
    ::= 'AGGREGATE' entity_type_ref
        'BY' group_expr ( ',' group_expr )*
        'COMPUTE' agg_expr ( ',' agg_expr )*
        [ 'WHERE' predicate_expr ]
        [ 'HAVING' predicate_expr ]
        [ 'AT' temporal_clause ]
        [ 'IN' world_ref ]
        [ 'ORDER' 'BY' order_expr ( ',' order_expr )* ]
        [ 'LIMIT' integer ]
    ;

(* === TIMELINE Statement: Event Timeline === *)
timeline_statement
    ::= 'TIMELINE' entity_ref
        [ 'EVENTS' event_type_list ]
        [ 'FROM' datetime_expr 'TO' datetime_expr ]
        [ 'WHERE' predicate_expr ]
        [ 'ORDER' 'BY' 'event_time' ( 'ASC' | 'DESC' ) ]
        [ 'LIMIT' integer ]
    ;

(* === DIFF Statement: Branch Comparison === *)
diff_statement
    ::= 'DIFF' entity_type_ref
        'BETWEEN' branch_ref 'AND' branch_ref
        [ 'WHERE' predicate_expr ]
    ;

#2.2 Expression Grammar

BNF
(* === Predicate Expressions === *)
predicate_expr
    ::= comparison_expr
      | predicate_expr 'AND' predicate_expr
      | predicate_expr 'OR' predicate_expr
      | 'NOT' predicate_expr
      | '(' predicate_expr ')'
      | exists_expr
      | contains_expr
      | search_expr
      | similar_expr
    ;

comparison_expr
    ::= property_ref comparator value_expr
    ;

comparator
    ::= '=' | '!=' | '>' | '>=' | '<' | '<='
      | 'IN' | 'NOT' 'IN'
      | 'LIKE' | 'NOT' 'LIKE'
      | 'IS' 'NULL' | 'IS' 'NOT' 'NULL'
      | 'BETWEEN' value_expr 'AND' value_expr
    ;

(* Full-text search *)
search_expr
    ::= 'SEARCH' '(' property_ref ',' string_literal ')'
    ;

(* Semantic similarity *)
similar_expr
    ::= 'SIMILAR' '(' string_literal ',' float_literal ')'
      | 'SIMILAR' '(' vector_literal ',' float_literal ')'
    ;

(* Existence check *)
exists_expr
    ::= 'EXISTS' '(' traverse_path [ 'WHERE' predicate_expr ] ')'
    ;

(* Array containment *)
contains_expr
    ::= property_ref 'CONTAINS' value_expr
      | property_ref 'CONTAINS' 'ANY' '(' value_list ')'
      | property_ref 'CONTAINS' 'ALL' '(' value_list ')'
    ;

(* === Property References === *)
property_ref
    ::= identifier                     (* top-level property *)
      | identifier '.' identifier      (* nested property *)
      | identifier '[' integer ']'     (* array index *)
    ;

(* === Graph Traversal Path === *)
traverse_path
    ::= '->' edge_type_ref [ '(' predicate_expr ')' ] '->' entity_type_ref
      | '<-' edge_type_ref [ '(' predicate_expr ')' ] '<-' entity_type_ref
      | '--' edge_type_ref [ '(' predicate_expr ')' ] '--' entity_type_ref
    ;

(* === Metrics and Computed Properties === *)
with_clause
    ::= 'METRIC' metric_name_list
      | 'COMPUTED' computed_prop_list
      | 'EDGES' edge_summary_list
    ;

(* === Aggregate Functions === *)
agg_expr
    ::= agg_function '(' property_ref ')' [ 'AS' alias ]
    ;

agg_function
    ::= 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
      | 'PERCENTILE' | 'STDDEV' | 'VARIANCE'
      | 'COUNT_DISTINCT' | 'TOPN'
      | 'TIME_BUCKET'
    ;

(* === Temporal Clause === *)
temporal_clause
    ::= 'TIME' datetime_expr
      | 'BRANCH' branch_name
      | 'SNAPSHOT' snapshot_id
    ;

(* === World Reference === *)
world_ref
    ::= 'WORLD' world_name
    ;

#3. Core Statements in Detail

#3.1 FETCH: Entity Queries

Code
FETCH is OQL's most fundamental query, analogous to SQL SELECT:

┌───────────────────────────────────────────────────────────┐
│                    FETCH Statement Structure                │
│                                                            │
│  FETCH <EntityType>                                        │
│    WHERE <conditions>          <- Filter predicates         │
│    WITH METRIC <metrics>       <- Metric expansion          │
│    AT TIME <timestamp>         <- Time travel               │
│    IN WORLD <world>            <- World scope               │
│    ORDER BY <fields>           <- Sorting                   │
│    LIMIT <n> OFFSET <m>        <- Pagination                │
└───────────────────────────────────────────────────────────┘

Example collection:

OQL
-- Basic query: Find all engineers over 30
FETCH Person
WHERE department = 'Engineering' AND age > 30
ORDER BY hire_date DESC
LIMIT 20;

-- Nested property query
FETCH Person
WHERE address.city = 'San Francisco' AND skills CONTAINS 'Python';

-- Full-text + semantic search combo
FETCH Document
WHERE SEARCH(content, 'risk assessment report')
  AND SIMILAR('machine learning models for credit risk', 0.8)
ORDER BY _score DESC
LIMIT 10;

-- Query with metric expansion
FETCH Person
WHERE department = 'Engineering'
WITH METRIC direct_reports, total_projects, avg_performance_score
ORDER BY direct_reports DESC;

-- Time travel query
FETCH Person
WHERE department = 'Engineering'
AT TIME '2023-06-01T00:00:00Z'
IN WORLD 'production';

-- Existence condition
FETCH Company
WHERE EXISTS(-> Employs -> Person WHERE role = 'CTO')
  AND industry = 'Technology';

#3.2 TRAVERSE: Graph Traversal

Code
TRAVERSE executes graph traversals, replacing multi-table JOINs:

┌───────────────────────────────────────────────────────────┐
│                  TRAVERSE Statement Structure               │
│                                                            │
│  TRAVERSE <start_entity>                                   │
│    -> <EdgeType> -> <EntityType>     <- Forward traversal   │
│    <- <EdgeType> <- <EntityType>     <- Reverse traversal   │
│    -- <EdgeType> -- <EntityType>     <- Bidirectional       │
│    WHERE <conditions>                <- Path conditions      │
│    DEPTH 1..3                        <- Traversal depth      │
│    LIMIT <n>                         <- Result limit         │
└───────────────────────────────────────────────────────────┘

Example collection:

OQL
-- Single-hop: Which company does a person work at
TRAVERSE Person('person-001')
  -> WorksAt -> Company;

-- Two-hop: All projects of a company's employees
TRAVERSE Company('company-001')
  <- WorksAt <- Person
  -> WorksOn -> Project;

-- Conditional traversal: Only senior employees
TRAVERSE Company('company-001')
  <- WorksAt(weight > 0.8) <- Person
WHERE level = 'Senior'
  -> Manages -> Person;

-- Variable depth: Org chart tree
TRAVERSE Person('ceo-001')
  -> Manages -> Person
DEPTH 1..5;

-- Bidirectional: Find connected entities
TRAVERSE Device('device-001')
  -- ConnectedTo -- Device
  -- LocatedAt -- Location
DEPTH 1..3
LIMIT 50;

-- Path pattern matching: Supply chain tracing
TRAVERSE Product('prod-001')
  <- SuppliedBy <- Supplier
  <- ManufacturedBy <- Factory
  -> LocatedIn -> Region
WHERE Region.country = 'China';

#3.3 AGGREGATE: Analytics

OQL
-- Headcount and average age by department
AGGREGATE Person
BY department
COMPUTE COUNT(*) AS headcount,
        AVG(age) AS avg_age,
        MAX(salary) AS max_salary
WHERE is_active = true
ORDER BY headcount DESC;

-- Event counts by time bucket
AGGREGATE Event
BY TIME_BUCKET(event_time, '1 HOUR') AS hour_bucket,
   event_type
COMPUTE COUNT(*) AS event_count,
        COUNT_DISTINCT(entity_id) AS unique_entities
WHERE severity IN ('error', 'critical')
  AND event_time BETWEEN '2024-06-01' AND '2024-06-30'
ORDER BY hour_bucket ASC;

-- Nested aggregation: TopN
AGGREGATE Transaction
BY source_account
COMPUTE SUM(amount) AS total_amount,
        COUNT(*) AS tx_count,
        TOPN(target_account, 5) AS top_targets
WHERE transaction_date >= '2024-01-01'
HAVING total_amount > 1000000
ORDER BY total_amount DESC
LIMIT 100;

#3.4 TIMELINE: Event Timeline

OQL
-- Complete entity timeline
TIMELINE Person('person-001')
FROM '2024-01-01' TO '2024-06-30'
ORDER BY event_time DESC
LIMIT 100;

-- Filter specific event types
TIMELINE Device('device-001')
EVENTS Alert, StatusChange, Measurement
FROM '2024-06-01' TO '2024-06-30'
WHERE severity IN ('error', 'critical')
ORDER BY event_time DESC;

-- Timeline with correlation tracking
TIMELINE Order('order-001')
EVENTS StatusChange
WHERE correlation_id = 'trace-abc-123'
ORDER BY event_time ASC;

#4. Metric Expansion Mechanism

#4.1 Metric Definitions

YAML
# metrics-registry/person-metrics.yaml
metrics:
  - name: direct_reports
    entity_type: Person
    description: "Number of direct reports"
    type: count
    definition:
      edge_type: Manages
      direction: outbound
      count: targets

  - name: total_projects
    entity_type: Person
    description: "Total projects involved in"
    type: count
    definition:
      edge_type: WorksOn
      direction: outbound
      count: targets

  - name: avg_performance_score
    entity_type: Person
    description: "Average performance score"
    type: aggregate
    definition:
      event_type: PerformanceReview
      field: payload.score
      function: avg
      time_range: last_12_months

  - name: alert_frequency
    entity_type: Device
    description: "Alert frequency (per day)"
    type: rate
    definition:
      event_type: Alert
      time_range: last_30_days
      unit: per_day

#4.2 Expansion Process

Code
Metric Expansion Compilation Process:

Input OQL:
  FETCH Person WHERE department = 'Eng'
  WITH METRIC direct_reports, total_projects

Step 1: Look up metric definitions
  direct_reports -> COUNT(edge WHERE Manages outbound)
  total_projects -> COUNT(edge WHERE WorksOn outbound)

Step 2: Generate subqueries
  ┌─────────────────────────────────────────────┐
  │ SELECT ec.*,                                 │
  │   (SELECT COUNT(*)                           │
  │    FROM entity_edge ee                       │
  │    WHERE ee.source_id = ec.entity_id         │
  │      AND ee.edge_type = 'Manages'            │
  │      AND ee.is_deleted = FALSE               │
  │   ) AS direct_reports,                       │
  │   (SELECT COUNT(*)                           │
  │    FROM entity_edge ee                       │
  │    WHERE ee.source_id = ec.entity_id         │
  │      AND ee.edge_type = 'WorksOn'            │
  │      AND ee.is_deleted = FALSE               │
  │   ) AS total_projects                        │
  │ FROM entity_common ec                        │
  │ WHERE ec.entity_type = 'Person'              │
  │   AND JSON_EXTRACT(ec.properties,            │
  │       '$.department') = 'Eng'                │
  └─────────────────────────────────────────────┘

Step 3: Optimize
  - Subqueries -> LEFT JOIN aggregation (for large datasets)
  - Check for pre-computed materialized views
  - Check computed_props cache

#5. Comparison with Other Query Languages

#5.1 Comparison Matrix

Code
OQL vs Other Query Languages:

┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ Feature       │   OQL    │   SQL    │  Cypher  │ GraphQL  │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│ Type-aware    │ Native   │ Manual   │ Labels   │ Schema   │
│ Graph traverse│ TRAVERSE │ Multi-   │ Native   │ Nested   │
│               │          │ JOIN     │          │          │
│ Aggregation   │ AGGREGATE│ Native   │ Weak     │ Weak     │
│ Full-text     │ SEARCH   │ Extension│ Plugin   │ None     │
│ Vector search │ SIMILAR  │ Extension│ None     │ None     │
│ Time travel   │ AT TIME  │ None     │ None     │ None     │
│ Branch diff   │ DIFF     │ None     │ None     │ None     │
│ Metric expand │ METRIC   │ Subquery │ None     │ None     │
│ Event timeline│ TIMELINE │ Manual   │ Path     │ None     │
│ Compiles to   │ SQL      │ N/A      │ No       │ Custom   │
│   SQL         │          │          │          │          │
│ OLAP optim.   │ Inherits │ Native   │ Weak     │ Weak     │
└──────────────┴──────────┴──────────┴──────────┴──────────┘

#5.2 Equivalent Query Comparison

Code
Same query in four languages:

Requirement: "Find all people managed by John, and the projects they work on"

OQL (4 lines):
  TRAVERSE Person('john')
    -> Manages -> Person
    -> WorksOn -> Project;

SQL (15 lines):
  SELECT p2.*, proj.*
  FROM entity_common p1
  JOIN entity_edge e1 ON e1.source_id = p1.entity_id
    AND e1.edge_type = 'Manages'
  JOIN entity_common p2 ON p2.entity_id = e1.target_id
  JOIN entity_edge e2 ON e2.source_id = p2.entity_id
    AND e2.edge_type = 'WorksOn'
  JOIN entity_common proj ON proj.entity_id = e2.target_id
  WHERE p1.entity_id = 'john'
    AND p1.world_id = 'main'
    AND e1.world_id = 'main'
    AND p2.world_id = 'main'
    AND e2.world_id = 'main'
    AND proj.world_id = 'main';

Cypher (6 lines):
  MATCH (p1:Person {id: 'john'})
    -[:Manages]->(p2:Person)
    -[:WorksOn]->(proj:Project)
  RETURN p2, proj;

GraphQL (12 lines):
  query {
    person(id: "john") {
      manages {
        name
        worksOn {
          name
          status
        }
      }
    }
  }

#6. Advanced Syntax Extensions

#6.1 Pipe Operator

OQL
-- The pipe operator |> enables chained processing
FETCH Person
WHERE department = 'Engineering'
|> TRAVERSE -> WorksOn -> Project
|> AGGREGATE BY Project.status
   COMPUTE COUNT(*) AS project_count;

-- Equivalent to nested queries but more readable

#6.2 Variable Binding

OQL
-- Use LET to bind intermediate results
LET engineers = FETCH Person WHERE department = 'Engineering';
LET high_performers = engineers WHERE performance_score > 90;

TRAVERSE high_performers
  -> Manages -> Person
WITH METRIC direct_reports;

#6.3 Pattern Matching

OQL
-- Pattern matching: Find "triangle" relationships
MATCH PATTERN
  (a:Person) -> Knows -> (b:Person),
  (b:Person) -> Knows -> (c:Person),
  (c:Person) -> Knows -> (a:Person)
WHERE a.department != b.department
  AND b.department != c.department
LIMIT 100;

#7. Type System

#7.1 Value Types

Code
OQL Value Type System:

Primitive Types:
├── String          "hello"
├── Integer         42
├── Float           3.14
├── Boolean         true / false
├── DateTime        '2024-06-15T10:30:00Z'
├── Date            '2024-06-15'
├── Duration        '30 DAYS' / '2 HOURS'
└── Null            NULL

Composite Types:
├── Array           [1, 2, 3] / ['a', 'b']
├── Map             {key: value}
├── Vector          VECTOR([0.1, 0.2, ...])
└── GeoPoint        GEO(31.2, 121.5)

Reference Types:
├── EntityRef       Person('id-001')
├── EdgeRef         Edge('edge-001')
├── WorldRef        WORLD('main')
└── BranchRef       BRANCH('feature-x')

#7.2 Function Library

OQL
-- String functions
FETCH Person WHERE LOWER(name) LIKE '%john%';
FETCH Person WHERE LENGTH(description) > 100;

-- Math functions
FETCH Device WHERE ABS(temperature - 25.0) < 2.0;

-- Date functions
FETCH Person WHERE YEAR(hire_date) = 2023;
FETCH Event WHERE event_time > NOW() - INTERVAL '7 DAYS';

-- Array functions
FETCH Person WHERE ARRAY_LENGTH(skills) >= 3;
FETCH Person WHERE ARRAY_OVERLAP(skills, ['Python', 'Java']);

-- Geo functions
FETCH Store WHERE GEO_DISTANCE(location, GEO(31.2, 121.5)) < 5000;

#8. Error Handling and Hints

#8.1 Syntax Error Messages

Code
OQL provides friendly error messages:

Input: FETCH Perso WHERE age > 30
Error: Unknown entity type 'Perso'. Did you mean 'Person'?
     FETCH Perso WHERE age > 30
           ^^^^^
     Available types: Person, Company, Device, Document

Input: FETCH Person WERE age > 30
Error: Expected 'WHERE', found 'WERE'. Did you mean 'WHERE'?
     FETCH Person WERE age > 30
                  ^^^^

Input: FETCH Person WHERE ages > 30
Error: Unknown property 'ages' on type 'Person'. Did you mean 'age'?
     Available properties: age (Int), name (String), email (String)...

#Key Takeaways

  1. OQL elevates Ontology semantics to first-class query language citizens: Entity types, relationship traversal, and event timelines all have dedicated syntax, eliminating the verbose JSON functions and multi-table JOINs required in SQL.

  2. Four core statements cover all query scenarios: FETCH (entity query), TRAVERSE (graph traversal), AGGREGATE (analytics), and TIMELINE (event timeline), plus DIFF (branch comparison) cover the complete spectrum of Ontology data access patterns.

  3. Metric expansion decouples queries from metric definitions: The WITH METRIC clause auto-expands predefined metrics, freeing users from subquery and aggregation implementation details.

  4. OQL compiles to SQL for execution: This preserves all Doris OLAP engine optimizations (materialized views, vectorized execution, columnar storage) — ease of use without sacrificing performance.

  5. Progressive complexity design: Simple queries are extremely concise (FETCH Person WHERE age > 30), while complex queries compose syntax elements gradually, creating a smooth learning curve.

#Next Article

The next article S3-07 "OQL Parser: From Text to AST" dives into the OQL parser implementation, covering the lexer, recursive-descent parser, AST construction, query optimizer, and execution plan generation.

Tags: #OQL #OntologyQueryLanguage #BNF #GraphTraversal #MetricExpansion #FETCH #TRAVERSE #AGGREGATE #TIMELINE #coomia-dip #DataFoundation