Query Language

ObjectQuel draws inspiration from QUEL, the pioneering query language from UC Berkeley's Ingres project. Unlike SQL's table-centric approach, ObjectQuel queries work directly with your domain entities using object-oriented syntax. Queries read naturally while leveraging powerful search capabilities including wildcards, regex, and full-text search.

Query Structure

Every ObjectQuel query follows this structure, with clauses written in this specific order:

Query configuration diagram showing the 7 parts of an ObjectQuel query: 1. Query Flags (optional), 2. Range Definitions (required), 3. Related Ranges (optional), 4. Retrieve Clause (required), 5. Where Clause (optional), 6. Sort Clause (optional), 7. Pagination (optional)
Both range definitions and retrieve clauses are required. Every variable used in your query must be declared with a range definition before it can be used in retrieve, where, or sort clauses.

Query Optimization Flags

Query flags modify execution behavior for specific optimization scenarios. Flags are placed before the first range definition.

@InValuesAreFinal

When you query using an IN clause on primary keys or unique identifiers, ObjectQuel normally cannot assume how many rows will match — the same ID might appear in a non-unique column, or the query planner may not know the column is unique. The @InValuesAreFinal flag tells ObjectQuel that each value in the IN clause will match at most one row, enabling it to stop scanning early once all values have been found:

$results = $entityManager->executeQuery("
    @InValuesAreFinal
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.productId in (:ids)
", ['ids' => [1, 2, 3, 4, 5]]);

When to use: Enable this flag when your IN clause targets a primary key or unique column and you expect at most one result per value. Do not use it on non-unique columns where multiple rows could match a single value.

Range Definitions

Range definitions declare which entities you're querying. Every variable referenced in your query must have a corresponding range definition:

// Single entity range
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p)
");

Relationship Traversal (Related Ranges)

To query related entities, use the via keyword to traverse relationships. You always reference the property on the source entity that defines the relationship — ObjectQuel handles the underlying join logic automatically. The syntax is identical regardless of relationship type (ManyToOne, OneToMany, etc.).

Traversing to Related Entities

// ManyToOne: OrderEntity has a 'user' property referencing UserEntity
$results = $entityManager->executeQuery("
    range of o is App\\Entity\\OrderEntity
    range of u is App\\Entity\\UserEntity via o.user
    retrieve (o, u.email) where o.createdAt > '2024-01-01'
");

// OneToMany: UserEntity has an 'orders' property (collection of OrderEntity)
$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    range of o is App\\Entity\\OrderEntity via u.orders
    retrieve (u.name, o.total) where o.status = 'completed'
");

In the first example, via o.user follows OrderEntity's user property to reach the related UserEntity. In the second, via u.orders follows UserEntity's orders collection to reach OrderEntity.

Deep Relationship Traversal

Chain multiple via clauses to traverse through several relationship levels. Each range declaration connects to the previous entity through its relationship property:

// Traversal path: User → Orders → Order Items
$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    range of o is App\\Entity\\OrderEntity via u.orders
    range of i is App\\Entity\\OrderItemEntity via o.items
    retrieve (u.name, i.quantity) where i.productId = 123
");

Retrieving Data

The retrieve clause specifies what data to return. You have three options, each suited to different use cases:

1. Retrieve Full Entities

Returns complete entity objects with all properties hydrated. Use this when you need the entire entity:

$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.active = true
");

foreach($results as $row) {
    $product = $row['p']; // Full ProductEntity object
    echo $product->getName();
    echo $product->getPrice();
}

2. Retrieve Specific Properties

Returns only the requested property values as scalars. Use this to minimize memory usage and improve performance:

$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p.name) where p.active = true
");

foreach($results as $row) {
    $name = $row['p.name']; // String value, not an object
    echo $name;
}

3. Mix Entities and Properties

Combines full objects with specific property values. Use this when you need complete entities from one range but only specific fields from related entities:

$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    range of c is App\\Entity\\CategoryEntity via p.category
    retrieve (p, c.name) where p.price < 100
");

foreach($results as $row) {
    $product = $row['p'];           // Full ProductEntity object
    $categoryName = $row['c.name']; // Category name as string
}

Filtering with Where Clauses

Build complex conditions using comparison operators and logical combinators:

Comparison Operators

// Range queries
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.price >= :minPrice and p.price <= :maxPrice
", ['minPrice' => 10.00, 'maxPrice' => 50.00]);

// Alternative using BETWEEN
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.price between :min and :max
", ['min' => 10.00, 'max' => 50.00]);

IN and NOT IN

// Match multiple values
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.categoryId in (:categories)
", ['categories' => [1, 2, 3]]);

// Exclude multiple values
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.status not in (:excluded)
", ['excluded' => ['deleted', 'archived']]);

NULL Handling

// IS NULL
$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    retrieve (u) where u.deletedAt is null
");

// IS NOT NULL
$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    retrieve (u) where u.emailVerifiedAt is not null
");

Parameter Binding

Always use parameter binding to prevent SQL injection and enable query plan caching.

// Single parameter
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.price < :maxPrice
", ['maxPrice' => 100.00]);

// Multiple parameters
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.price between :min and :max
", [
    'min' => 10.00,
    'max' => 100.00
]);

Sorting Results

Control result ordering with the sort by clause. When specifying multiple fields, they are evaluated left-to-right as tie-breakers:

// Single field
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.active = true
    sort by p.name asc
");

// Multiple fields (primary sort: featured, then price, then name)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.active = true
    sort by p.featured desc, p.price asc, p.name asc
");

// Sort by related entity properties
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    range of c is App\\Entity\\CategoryEntity via p.category
    retrieve (p) where p.active = true
    sort by c.name asc, p.name asc
");

Pagination

Handle large result sets using the window operator, which provides page-based access to results. ObjectQuel calculates the offset as page_number × items_per_page, so window 0, 10 retrieves items 1–10, window 1, 10 retrieves items 11–20, and window 2, 25 retrieves items 51–75.

// Syntax: window [page_number], [items_per_page]
// Page numbers are 0-indexed (first page = 0)

// First page, 10 items per page
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.active = true
    sort by p.name asc
    window 0, 10
");

// Third page, 25 items per page (items 51–75)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.active = true
    sort by p.name asc
    window 2, 25
");
Always sort before paginating. Without a sort by clause, the database may return rows in an unpredictable order, causing items to shift between pages or appear more than once.