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

ObjectQuel queries execute in this specific order. Understanding this flow is essential:

@InValuesAreFinal                              // 1. Query Flags (optional)
range of p is ProductEntity                    // 2. Range Definitions (required)
range of c is CategoryEntity via p.category    // 3. Related Ranges (optional)
retrieve (p, c.name)                           // 4. Retrieve Clause (required)
where p.price < :max and c.active = true       // 5. Where Clause (optional)
sort by c.name asc, p.price desc               // 6. Sort Clause (optional)
window 0, 20                                   // 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.

Range Definitions

Range definitions declare which entities you're querying and are mandatory for all queries. 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)
");

// Multiple ranges (explicit join)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    range of c is App\\Entity\\CategoryEntity via p.category
    retrieve (p, c)
");

When querying related entities, use the via keyword to explicitly traverse relationships. This makes the query intent clear and allows ObjectQuel to construct the appropriate joins.

Retrieving Data

The retrieve clause specifies what data to return. You have three options, each optimized for 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 table 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
}

Parameter Binding

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

Named Parameters

// 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
]);

Parameters in Complex Queries

$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    range of o is App\\Entity\\OrderEntity via u.orders
    retrieve (u, o.total)
    where u.registeredAt >= :startDate
      and o.status = :status
    sort by o.createdAt desc
", [
    'startDate' => '2024-01-01',
    'status' => 'completed'
]);

Security note: Never concatenate user input directly into queries. Always use parameter binding.

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
");

Complex Logical Conditions

// Combine with AND/OR/NOT
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p)
    where (p.featured = true or p.onSale = true)
      and p.stock > 0
      and not p.discontinued = true
");

Pattern Matching and Search

ObjectQuel provides three pattern matching approaches, each optimized for different scenarios:

Wildcard Patterns

Use asterisks (*) for simple pattern matching. This is the most performant option for prefix/suffix/contains searches:

// Prefix matching (most efficient)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.name = 'iPhone*'
");

// Suffix matching
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.name = '*Pro'
");

// Contains (use sparingly—less efficient)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.name = '*wireless*'
");

// Complex patterns: starts with ABC, has 3 chars, ends with XYZ
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.sku = 'ABC*-???-*XYZ'
");

Regular Expressions

Use regex for complex pattern requirements. Enclose patterns in forward slashes:

// Email validation
$results = $entityManager->executeQuery("
    range of u is App\\Entity\\UserEntity
    retrieve (u) where u.email = /^[a-zA-Z0-9._%+-]+@company\.com$/
");

// Case-insensitive matching (use 'i' flag)
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.description = /smartphone/i
");

Full-Text Search

Use the search() function for natural language queries. This leverages database full-text indexing:

// Single field search with boolean operators
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where search(p.description, 'wireless +bluetooth -expensive')
");
// +word = must include
// -word = must not include
// word = optional (boosts ranking)

// Multi-field search
$results = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where search(p.name, p.description, 'premium quality')
");

Performance tip: Use wildcards for exact patterns, regex for complex validation, and full-text search for natural language queries.

Relationship Traversal

ObjectQuel uses the via keyword to traverse relationships by referencing the relationship property on your entity. The syntax is identical regardless of relationship direction:

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 both cases, you reference the property name on the source entity: via o.user uses OrderEntity's user property, while via u.orders uses UserEntity's orders property. ObjectQuel handles the underlying join logic automatically.

Deep Relationship Traversal

Chain multiple via clauses to traverse through multiple relationship levels:

$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
");

This traverses: User → Orders (via orders property) → Order Items (via items property), filtering items by product ID.

Sorting Results

Control result ordering with the sort by clause. Multiple sort fields are evaluated left-to-right:

// 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 (priority order: featured → price → 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 efficiently using the window operator. The window operator calculates page boundaries rather than using direct LIMIT/OFFSET:

// Syntax: window [window_number], [items_per_window]
// Window numbers are 0-indexed (first page = window 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 (window 2 = 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 for consistent results
// Sorting is not strictly required but highly recommended

How it works: ObjectQuel calculates the offset as window_number × items_per_window. So window 0, 10 retrieves items 1-10, window 1, 10 retrieves items 11-20, and window 2, 25 retrieves items 51-75.

Query Optimization Flags

Query flags modify execution behavior for specific optimization scenarios:

@InValuesAreFinal

Optimizes IN queries when values are primary keys or unique identifiers. This tells ObjectQuel that results won't exceed the IN clause size:

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

// Without @InValuesAreFinal, ObjectQuel may use less efficient query plans
// Use this flag when querying by ID and you need all matching records

When to use: Enable this flag when your IN clause contains primary keys and you expect at most one result per value.