Query language (PQL)
PQL, the Peryx Query Language, provides one read surface over operational state. POST /+query accepts query text and
parameters, then returns a bounded page of typed rows. The language supports selection, filtering, ordering,
aggregation, joins, and pagination. It has no mutation operations.
PQL selects structured metadata. Ranked text search remains available through /+search.
Syntax
from <domain>
[ where <predicate> ]
[ select <field> [, <field> ...] ]
[ aggregate <func>(<column>) as <alias> [, ...] by <key> [, ...] ]
[ order by <field> [asc|desc] [, ...] ]
[ limit <n> ]
Predicates support and, or, not, parentheses, comparisons, membership with in (...), and prefix matching with
starts_with. PQL has no arithmetic, function calls, or leading-wildcard matches. The planner checks query cost before
execution.
Literal forms include strings, integers, booleans, and RFC 3339 timestamps prefixed with @, such as
@2026-06-01T00:00:00Z.
Bind caller values through named parameters. The evaluator fills a :name placeholder from params; parameter values
cannot change query structure.
curl -u alice:$PASSWORD \
-H 'content-type: application/json' \
-d '{
"query": "from policy.decisions where repository == :repo and state == \"deny\" order by evaluated_at desc limit 25",
"params": {"repo": "team-cache"}
}' \
http://127.0.0.1:4433/+query
The response contains rows and an optional cursor:
{
"rows": [
{
"repository": "team-cache",
"resource": "component-a",
"state": "deny",
"action": "serve",
"evaluated_at": 1800000000,
"fresh": true
}
],
"next_cursor": null
}Domains
PQL provides two typed domains:
policy.decisions: bounded repository policy historyusage.reads: durable read and byte totals by repository resource
policy.decisions
| Column | Type | Meaning |
|---|---|---|
repository, resource | string | Shared repository and owner-normalized resource |
group, artifact | string | Optional owner-supplied grouping and artifact identity |
action | string | Evaluated operation |
state | string | allow, deny, or wait |
evaluated_at | timestamp | Evaluation time in whole Unix seconds |
fresh | bool | Current repository, catalog, and policy generations match |
source, rule, reason | string | Routed source, matched rule, and explanation; operator access required |
usage.reads
| Column | Type | Meaning |
|---|---|---|
repository, resource | string | Repository and resource |
reads | int | Lifetime artifact reads served |
bytes | int | Lifetime bytes served; operator access required |
select * and an omitted select return all columns visible to the caller. A named selection returns those visible
columns. Default ordering uses descending evaluated_at for policy decisions and descending reads for usage.
Aggregation
PQL supports count, sum, min, and max over declared numeric columns, grouped by declared keys.
curl -u alice:$PASSWORD \
-H 'content-type: application/json' \
-d '{"query": "from policy.decisions aggregate count() as decisions by state"}' \
http://127.0.0.1:4433/+query
Use /+analytics/timeline for time buckets.
Joins
A query can join two domains on declared shared keys. Joins use inner semantics: an outer row appears after a match in the joined domain.
curl -u alice:$PASSWORD \
-H 'content-type: application/json' \
-d '{"query": "from policy.decisions join usage.reads on repository, resource where state == \"deny\" order by reads desc limit 25"}' \
http://127.0.0.1:4433/+query
The planner admits a join when the joined domain indexes all join keys. It rejects a join that requires an unbounded
scan with 400. A field present on either side keeps the stricter visibility class.
Authorization
The evaluator resolves the caller credential and adds a repository predicate before ordering and pagination. Query text cannot name or remove that predicate.
Filter repository by its configured repository name, which is the value stored in grants and records. A repository
credential or reader can query its granted repository. An administrator can omit that filter for operator-wide data. A
caller without domain access receives 404.
Repository-scoped callers see repository fields. Operator fields such as source, rule, and reason are omitted.
Responses containing operator fields use Cache-Control: no-store; repository-level responses use private, no-cache.
Pagination
A page with more matches includes next_cursor. Send it as the request cursor to continue. The cursor binds to the
authorization scope that created it. A grant change causes the caller's scope changed; restart the query.
Limits and errors
The default page size is 25, with a maximum of 100. Query text size, predicate depth, and execution cost have fixed bounds.
| Result | Meaning |
|---|---|
400 | Parse, validation, budget, join, or cursor error |
401 | Missing or invalid credential |
404 | Caller cannot read the domain |
415 | Request body does not use application/json |
422 | Malformed JSON or unknown body field |
503 | Query backend unavailable |
Error bodies omit query text and parameter values.