Querying
██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
Queries inspect the final solve result. After solve_once succeeds, the caller
asks whether specific ground facts are present among the derived IDB facts.
solve_once()
↓
query_solved_ground_fact() (repeat as needed)
↓
solve_result_free()Why querying exists
The solver produces an opaque result containing all derived IDB facts. Querying is how the caller extracts the authorization decision from that result. Without querying, a solve result is a black box.
solve result = all facts the policy derived
query = "was allow(alice, roadmap.pdf) derived?"
answer = true or falseQueries are explicit and ground — the caller names the exact fact to check. This keeps the interface minimal and the access surface controlled.
Where querying fits
The Public Query Whitelist (for manifest-loaded policies) controls which predicates the caller is allowed to inspect. See Manifest Loading for whitelist configuration.
Query API
maelys_result_t
maelys_datalog_query_solved_ground_fact(
const maelys_datalog_solve_result_t *result,
const char *predicate,
const maelys_datalog_term_t *terms,
size_t arity,
bool *out_present);| Parameter | Meaning |
|---|---|
result | Solve result from solve_once. Must not be NULL. |
predicate | Predicate name to query, for example "allow". Must not be NULL. |
terms | Array of ground terms. |
arity | Number of terms. Must match the predicate's declared arity. |
out_present | Set to true if the fact is present, false otherwise. Must not be NULL. |
Ground-only queries
Queries are ground — every term must be fully bound. Variables are not allowed in query terms. The query API checks for the exact fact, not for any fact that matches a pattern.
Allowed: allow("alice", "roadmap.pdf") — fully ground
Not allowed: allow(X, "roadmap.pdf") — variable XEnumerating a whole relation
A pattern with a variable — "all X such that allow(X, "roadmap.pdf")" —
is not a ground query. Testing it with query_solved_ground_fact alone would
require already knowing every candidate X to check, one call at a time.
maelys_datalog_solve_result_enumerate_predicate_facts reads back every
already-derived fact of a QUERY-flagged predicate directly, without
guessing ground terms in advance:
maelys_result_t
maelys_datalog_solve_result_enumerate_predicate_facts(
const maelys_datalog_solve_result_t *result,
const char *predicate,
size_t arity,
maelys_datalog_fact_t *out_facts,
size_t out_capacity,
size_t *out_count);| Parameter | Meaning |
|---|---|
result | Solve result from solve_once. Must not be NULL. |
predicate | Predicate name to enumerate, for example "allow". |
arity | Number of terms per fact. Must match the predicate's declared arity. |
out_facts | Caller-provided buffer of out_capacity facts. May be NULL only if out_capacity == 0 (count-only mode). |
out_capacity | Number of facts out_facts can hold. |
out_count | Set to the total number of matching facts, even if it exceeds out_capacity — compare *out_count > out_capacity to detect truncation. |
A pattern query with a variable is not a separate primitive — enumerate the predicate in full, then filter the returned facts on the caller side:
allow(X, "roadmap.pdf")
→ enumerate_predicate_facts(result, "allow", 2, ...)
→ filter facts where terms[1] == doc_sid
→ X = the remaining terms[0] valuesThe same access control as ground queries applies: the predicate must be
QUERY-flagged, and manifest-loaded policies still enforce the Public Query
Whitelist. Results are maelys_datalog_fact_t — symbol IDs, never resolved
to strings; enumerates only already-derived IDB facts, not EDB or
policy-source facts. This accessor is available at the C API level; a
WASM/JS binding is not implemented yet.
Calling with out_capacity == 0 and out_facts == NULL returns only the
count, letting the caller size a buffer before a second call — the same
count-then-fetch pattern used elsewhere in this API.
Example: symbol query
maelys_result_t rc;
bool allowed = false;
/* Intern the symbol values */
maelys_datalog_symbol_id_t user_sid, doc_sid;
maelys_datalog_symbol_intern(
&policy_set.policies[0].symbols,
"alice", 5, &user_sid);
maelys_datalog_symbol_intern(
&policy_set.policies[0].symbols,
"roadmap.pdf", 11, &doc_sid);
/* Build the query terms */
maelys_datalog_term_t terms[2];
terms[0].kind = MAELYS_DATALOG_TERM_SYMBOL; terms[0].as.symbol = user_sid;
terms[1].kind = MAELYS_DATALOG_TERM_SYMBOL; terms[1].as.symbol = doc_sid;
/* Query */
rc = maelys_datalog_query_solved_ground_fact(
result, "allow", terms, 2, &allowed);
if (rc == MAELYS_OK && allowed) {
/* allow("alice", "roadmap.pdf") is derived */
}Symbol lookup for queries
Query terms use symbol IDs, not raw strings. To query for a symbol value that
may or may not have been interned during EDB construction, use
maelys_datalog_symbol_intern. If the symbol is absent from the table, interning
adds it, and the fact obviously won't be found — the query correctly returns
false.
Alternatively, to avoid interning unknown symbols into the shared table, scan the existing symbol table first. This is only relevant in highly constrained environments. In typical usage, interning is idempotent and safe.
Symbol interning is idempotent. Interning the same string twice returns the same ID. The symbol table grows monotonically — symbols are never removed.
Example: integer query
maelys_datalog_term_t terms[2];
maelys_datalog_symbol_id_t user_sid;
maelys_datalog_symbol_intern(
&policy_set.policies[0].symbols,
"alice", 5, &user_sid);
terms[0].kind = MAELYS_DATALOG_TERM_SYMBOL;
terms[0].as.symbol = user_sid;
terms[1].kind = MAELYS_DATALOG_TERM_INT;
terms[1].as.integer = 1024; /* file size limit */
bool present = false;
maelys_datalog_query_solved_ground_fact(
result, "quota_exceeded", terms, 2, &present);Example: 1-arity boolean query
maelys_datalog_term_t term;
maelys_datalog_symbol_id_t sid;
maelys_datalog_symbol_intern(
&policy_set.policies[0].symbols,
"req-1", 5, &sid);
term.kind = MAELYS_DATALOG_TERM_SYMBOL;
term.as.symbol = sid;
bool allowed = false;
maelys_datalog_query_solved_ground_fact(
result, "allow", &term, 1, &allowed);Public Query Whitelist
For manifest-loaded policy sets, the queries field is a Public Query
Whitelist. Only predicates listed in queries are publicly observable.
Querying a predicate that is not whitelisted returns MAELYS_ERR_FORBIDDEN.
{
"queries": [
{ "name": "allow", "arity": 2 }
]
}Querying allow/2 → permitted (in whitelist)
Querying debug/2 → MAELYS_ERR_FORBIDDEN (not in whitelist)The whitelist restricts public observation, not solver computation. Predicates absent from the whitelist are still evaluated internally by the solver and may contribute to deriving whitelisted facts.
The whitelist is enforced at query time, not at solve time. The solver always evaluates all QUERY predicates. The whitelist determines what the caller is allowed to inspect.
Inline loading — no whitelist
For inline-loaded policy sets, there is no Public Query Whitelist. Any
predicate declared as QUERY by the selected domain is accessible.
/* Inline loading — all domain QUERY predicates accessible */
maelys_datalog_load_policy_inline(
"access", "access.inline", src, src_len,
0, &policy_set, &diag);
/* Querying allow/1 — permitted if declared QUERY by domain */
maelys_datalog_query_solved_ground_fact(
result, "allow", &term, 1, &present);The WASM dynamic builder uses inline loading semantics — all domain QUERY
predicates are accessible through query_symbol without a whitelist. See
WASM bindings for the JavaScript wrapper.
Multiple queries on one result
A solve result may be queried multiple times. Each call to
query_solved_ground_fact is independent. The result is not modified by
querying.
bool alice_allowed = false;
bool bob_allowed = false;
maelys_datalog_query_solved_ground_fact(
result, "allow", alice_terms, 2, &alice_allowed);
maelys_datalog_query_solved_ground_fact(
result, "allow", bob_terms, 2, &bob_allowed);
maelys_datalog_solve_result_free(result);Freeing the result
Always free the solve result after all queries are done.
maelys_datalog_solve_result_free(result);Passing NULL is safe. Do not query the result after freeing it.
Return values
| Return code | Meaning |
|---|---|
MAELYS_OK | Query succeeded. *out_present is valid. |
MAELYS_ERR_INVALID_ARGUMENT | NULL pointer, arity mismatch, or variable term in query. |
MAELYS_ERR_INVALID_FIELD | Unknown predicate or arity mismatch with domain declaration. |
MAELYS_ERR_FORBIDDEN | Predicate exists but is not in the manifest Public Query Whitelist. |
MAELYS_OK with *out_present = false means the fact is absent. It is not
an error. The solver completed normally and the queried fact was not derived.
Summary
query_solved_ground_fact() → check if one specific ground fact is derivedQueries are ground and exact — they check a single specific fact. The whitelist
controls which predicates are publicly observable for manifest-loaded policies.
Inline-loaded policies expose all domain QUERY predicates without restriction.