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.
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
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.
KQL clauses are indentation-scoped and appear in this order. Only from is mandatory.
| Clause | Purpose |
|---|---|
select | Optional typed projection. Without it, the root entity is selected. |
from | Entity root for query(em):, or typed Queryable<T> plus mandatory alias for query:. |
join | Ordinary inner, outer, cross, or relationship fetch join. |
where | Boolean filter expression. |
group by | One or more typed grouping expressions. |
having | Boolean filter evaluated over groups and aggregates. |
order by | One or more typed sort expressions. |
offset | Non-negative integral number of rows to skip. |
limit | Non-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.
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 nullandis not nullare the explicit null predicates.- An empty collection used with
inproduces no matching rows. - Only compiler-recognized translatable operations may execute inside a provider query.
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.
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.
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
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.
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.
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.
Creating a Query<T> performs no provider I/O. Execute it explicitly with a terminal
operation.
| Operation | Result 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)
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.
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.
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.
- 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.
Continue with Entity Engine and XML to apply typed KQL to an atomic document source.