Databases & Entities KQL Static queries

Entity Engine KQL

KQL, the Klyn Query Language, is a native statically typed sublanguage. The compiler resolves entities and members, infers the exact result type, validates clause semantics, and lowers one immutable query plan that SQL, memory, XML, and JSON providers execute without silent fallback.

Manager-Bound Queries

Prefer query(em): when an EntityManager owns the persistence context. The from clause names one entity from the factory model; the compiler creates the typed repository root automatically.

minimum = 50.0
maximum = 150.0

articles = query(em):
    from Article
    where minimum <= price <= maximum
    order by price asc

With one manager-bound root, the alias is optional. Unqualified names such as price resolve to mapped properties. Declare an alias when it improves readability or when an outer variable has the same name as a property.

price = 100.0

articles = query(em):
    from Article article
    where article.price <= price
Ambiguous names are rejected

If both the query root and the surrounding Klyn scope define price, an unqualified use is a compile-time error. Add an alias and qualify the entity member explicitly.

Clause Order

KQL clauses are indentation-scoped and appear in this order. Only from is mandatory.

ClausePurpose
selectOptional typed projection. Without it, the root entity is selected.
fromEntity root for query(em):, or typed Queryable<T> plus mandatory alias for query:.
joinOrdinary inner, outer, cross, or relationship fetch join.
whereBoolean filter expression.
group byOne or more typed grouping expressions.
havingBoolean filter evaluated over groups and aggregates.
order byOne or more typed sort expressions.
offsetNon-negative integral number of rows to skip.
limitNon-negative integral maximum result count.
page = query(em):
    select article.id, article.description, article.price
    from Article article
    where article.price >= minimum
    order by article.price desc, article.id asc
    offset pageIndex * pageSize
    limit pageSize

A provider validates the complete plan before execution. If a dialect cannot implement a requested join or another operation, it raises ProviderCapabilityException rather than changing the query or evaluating unsupported work silently on the client.

Filtering

A where expression must have type Boolean. It uses ordinary typed Klyn operators over mapped values: equality, ordered comparisons, and, or, not, in, chained ranges, and null tests.

availableBrands = s:["Klyn Labs", "Deep Systems"]

matching = query(em):
    from Article article
    where article.brand in availableBrands
      and 20.0 <= article.price <= 200.0
      and article.description is not null
  • Entity equality compares mapped identities, not object addresses.
  • is null and is not null are the explicit null predicates.
  • An empty collection used with in produces no matching rows.
  • Only compiler-recognized translatable operations may execute inside a provider query.
Joins, Grouping, and Correlated Subqueries

Ordinary joins combine independent typed sources. join, left join, right join, and full outer join use an on predicate; cross join deliberately has none. Outer-join values that may be absent must be handled explicitly, for example with coalesce.

customers = em.repository<Customer>()
orders = em.repository<Order>()
minimumTotal = 25.0

report = query:
    select customer.name, count(order.id), coalesce(sum(order.total), 0.0)
    from customers customer
    left join orders order on order.customerId == customer.id
    group by customer.id, customer.name
    having exists(
        query:
            from orders candidate
            where candidate.customerId == customer.id
              and candidate.total > minimumTotal
    )

KQL recognizes count, sum, avg, min, and max as aggregate operations. Every selected non-aggregate entity expression must also appear in group by, aggregate functions cannot be nested, and aggregate predicates belong in having, not where. The nested query above is correlated because it refers to the outer customer alias.

Capabilities remain provider-specific

Memory supports inner, left, right, full outer, and cross joins. SQL support is checked against the selected dialect; for example, MariaDB rejects a native full outer join. Inspect query.explain().capabilities when portable code needs to select an alternative plan.

Projection and Result Types

Omitting select produces Query<Entity>. One selected expression produces its exact type. Multiple expressions produce a typed tuple. A constructor expression produces the constructed value type.

# Query<Article>
entities = query(em):
    from Article
    where price > 0.0

# Query<String>
names = query(em):
    select article.description
    from Article article
    order by article.description

# Query<Tuple<Int, String>>
identityAndName = query(em):
    select article.id, article.description
    from Article article

Use select distinct ... when duplicates must be removed explicitly. KQL otherwise preserves bag semantics and does not discard duplicate result rows.

brands = query(em):
    select distinct article.brand
    from Article article
    order by article.brand
Ordering and Pagination

Result order is unspecified without order by. Each term may use asc or desc, and nullable terms may request nulls first or nulls last when the provider supports that placement.

recent = query(em):
    from Article article
    order by article.updatedAt desc nulls last, article.id asc
    offset 0
    limit 25

Add a stable unique tie-breaker before pagination. offset and limit accept integral expressions and captured parameters, not floating-point values.

Fetching Relationships

join fetch requests relationship materialization while preserving the source entity as the complete projection. Inner and left fetch semantics are supported.

blog = (query(em):
    from Blog blog
        left join fetch blog.posts
    where blog.identifier == requestedId
).single()

A fetch path must begin at the root alias and every segment must be a mapped relationship. It introduces no alias of its own. See Entity Engine Relations for ownership and lazy-loading rules.

Repository Form

The explicit repository form is retained for reusable provider-independent functions and for direct XML or JSON repositories. Its source alias is mandatory.

def affordable(
    source as Queryable<Article>,
    maximum as Double
) as Query<Article>:
    return query:
        from source item
        where item.price <= maximum
        order by item.price

articles = em.repository<Article>()
selected = affordable(articles, 100.0)

Both forms lower to the same query representation. A repository exposes queries; entity lifecycle operations remain on EntityManager.

Lazy Execution

Creating a Query<T> performs no provider I/O. Execute it explicitly with a terminal operation.

OperationResult and contract
toList()Materializes a List<T>.
toSet()Materializes a Set<T>.
first() / firstOrNull()Returns the first provider result; use explicit ordering when the choice must be deterministic.
single() / singleOrNull()Enforces zero/one cardinality and rejects multiple rows.
count()Returns an optimized Long count.
exists()Stops as soon as one matching row is found.
page() / slice()Executes typed pagination requests.
stream()Returns a sequential forward-only resource that must be closed.
explain()Returns a redacted provider explanation.
try rows = selected.stream():
    while rows.next():
        print(rows.current)
Mandatory Entity Policies

An EntityPolicy<T> is a typed mandatory filter compiled to canonical QIR before provider lowering. Register policies on the factory so the same rule applies to KQL, find<T>(), mutations, relationship loading, and identity-map lookups.

@QueryPolicy
class VisibleArticlePolicy extends EntityPolicy<Article>:
    public override apply(
        source as Queryable<Article>,
        context as PolicyContext
    ) as Query<Article>:
        return query:
            from source article
            where not article.deleted

factory = EntityManagerFactory(
    configuration,
    entities=[Article],
    policies=[VisibleArticlePolicy()]
)

Policy code must preserve the protected entity shape and may only add a filter. It cannot join, group, order, paginate, project, or fetch relationships. An exceptional bypass requires an application-supplied PolicyAuthorization; withoutPolicy(...) verifies the exact policy type and emits mandatory audit events when the scope opens and closes.

Explicit Raw SQL Boundary

SQL applications may use sqlQuery<T>() and sqlExecute() for operations that cannot be expressed in KQL. This is an explicit dialect-specific boundary: a raw query is not a Queryable<T> and cannot be composed with KQL. Values remain bound through SQLParameters; never interpolate them into SQL text.

rows = em.sqlQuery<Article>(
    "select id, description, price from T_Articles where price > ? order by id",
    SQLParameters(minimum)
).toList()

sqlExecute() requires an active manager transaction. Active entity policies also apply at this boundary: raw SQL is rejected when policy injection cannot be proven safe. A bypass requires an explicit, authorized, and audited withoutPolicy(...) scope.

Captured Values

Query creation captures scalar and immutable values by value, entities by mapped identity, and collections as immutable copies. Use an explicit Parameter<T> only when a value must be supplied after the query has been created.

minimum = 10.0

queryPlan = query(em):
    from Article
    where price >= minimum

minimum = 1000.0
# queryPlan still contains the captured value 10.0.
Static Safety and Provider Boundaries
  • Unknown entities, properties, and aliases are compiler errors.
  • Clause order and indentation are validated by the Klyn parser.
  • Predicate, projection, ordering, offset, and limit types are inferred statically.
  • Join predicates, grouping expressions, aggregate placement, and correlated aliases are validated statically.
  • Unsupported operations produce diagnostics; no arbitrary method runs locally per row.
  • Queries from different provider instances cannot be combined accidentally.
  • Provider values are always bound as parameters, never concatenated into generated SQL.
Next Step

Continue with Entity Engine and XML to apply typed KQL to an atomic document source.