Python
Querying
Querying in the unified Python binding.The Python querying reference continues the Python overview.
Classes
| Class | Kind | Role |
|---|---|---|
SolveResult | Resource-owning class | Retains a solved model for queries, enumeration and explanations. |
ResultTerm | Immutable value class | Immutable raw term tied to its live result. |
Methods
| Method | Purpose |
|---|---|
SolveResult.contains_fact() | Check one concrete fact |
SolveResult.derived_fact_count() | Count the derived facts |
SolveResult.enumerate_predicate_facts() | Copy resolved result rows |
SolveResult.enumerate_raw() | Read result-owned term views |
SolveResult.resolve_term() | Resolve a term through its owner |
ResultTerm.resolve() | Resolve this raw term |
SolveResult.explain_true() | Explain a derived fact |
SolveResult.explain_false() | Explain an absent fact |
SolveResult.close() | Release a solved result |
Properties and attributes
Read properties without parentheses. Their links lead to the class card defined on this page.
| Property / protocol | Type | Meaning |
|---|---|---|
result.fingerprint | str | Selected-policy authority. |
result.execution_fingerprint | str | Selected policy plus execution configuration. |
term.kind | str | symbol, integer or boolean. |
term.value | int | bool | Result-local symbol ID, integer or boolean according to kind. |
Detailed API reference
Every class and method indexed at the start of this page has its detailed card here. A class’s method list also links to related operations documented on other topic pages; those links do not duplicate their cards.
Value-class declarations and public creation signatures below come from the published binding inventory. Resource-owning classes list their methods as individual links with a short explanation. Each link opens the complete signature, arguments, return behavior and example; the creation signatures are a reference, not executable class source. The examples are fragments: engine, ruleset, edb, session and result refer to the live objects explained on this page and in the complete examples. Import the named classes before using them.
SolveResult reference
Ruleset.solve(
edb: Edb,
*,
policy_index: int = 0,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0,
) -> SolveResult
Session.solve(
edb: Edb,
) -> SolveResult- SolveResult.close()Release a solved result
- SolveResult.contains_fact()Check one concrete fact
- SolveResult.derived_fact_count()Count the derived facts
- SolveResult.enumerate_predicate_facts()Copy resolved result rows
- SolveResult.enumerate_raw()Read result-owned term views
- SolveResult.explain_false()Explain an absent fact
- SolveResult.explain_true()Explain a derived fact
- SolveResult.resolve_term()Resolve a term through its owner
with session.solve(edb) as result:
print(result.fingerprint)
print(result.execution_fingerprint)
print(result.contains_fact("allow", ["alice", "roadmap.pdf"]))Creating a SolveResult
Obtain this object through one of these methods. Do not call its internal handle-taking constructor.
| Operation | Purpose |
|---|---|
Ruleset.solve() | Solve with a private session |
Session.solve() | Evaluate one complete request |
SolveResult attributes
Read these attributes without parentheses; they are not callable methods.
| Attribute / protocol | Type | Meaning |
|---|---|---|
result.fingerprint | str | Selected-policy authority. |
result.execution_fingerprint | str | Selected policy plus execution configuration. |
ResultTerm reference
@dataclass(frozen=True)
class ResultTerm:
kind: str
value: int | bool
_owner: SolveResult = field(repr=False)term = result.enumerate_raw("allow", 2)[0][0]
print(term.kind, term.value)
print(term.resolve())Check one concrete fact
Ask whether a fully specified, query-permitted fact is present in the solved model. This does not run the solver again.
SolveResult.contains_fact(
predicate: str,
terms: Sequence[object]
) -> boolboolReturns True for a present fact and False for an absent fact. Invalid predicates, arity or query permissions raise MaelysDatalogError, not False. Runtime EDB, policy facts and derived IDB may be queried when permitted.
allowed = result.contains_fact("allow", ["alice", "roadmap.pdf"])Count the derived facts
Read the total number of distinct IDB facts already derived in this result; this is not the number of input entries.
SolveResult.derived_fact_count() -> intintReturns an int. It does not derive more facts or enumerate the model. A closed result raises RuntimeError; native failures raise MaelysDatalogError.
print(result.derived_fact_count())Copy resolved result rows
Enumerate a queryable derived predicate when you need its rows rather than a single yes/no decision.
SolveResult.enumerate_predicate_facts(
predicate: str,
arity: int
) -> list[tuple[object, ...]]list[tuple[object, ...]]Returns a list of tuples containing str, int and bool values, in native enumeration order. The copies survive result closure. Only derived IDB facts are enumerated; invalid queries raise exceptions.
rows = result.enumerate_predicate_facts("allow", 2)
users = [user for user, doc in rows if doc == "roadmap.pdf"]Read result-owned term views
Use raw enumeration to inspect kinds and result-local identity without resolving every symbol immediately.
SolveResult.enumerate_raw(
predicate: str,
arity: int
) -> list[tuple[ResultTerm, ...]]list[tuple[ResultTerm, ...]]Returns tuples of immutable ResultTerm values. Symbol IDs belong only to this live result and cannot be used as later inputs. Resolve them before closing the result.
rows = result.enumerate_raw("allow", 2)
for user, document in rows:
print(user.kind, user.resolve(), document.resolve())Resolve a term through its owner
Convert a raw output term to a plain Python value through the same live result that produced it.
SolveResult.resolve_term(
term: ResultTerm
) -> objectobjectReturns str for a symbol, int for an integer, or bool for a boolean. A foreign or non-ResultTerm value raises ValueError; use after result closure raises RuntimeError.
term = result.enumerate_raw("allow", 2)[0][0]
user = result.resolve_term(term)Resolve this raw term
This is the convenient form of result.resolve_term(term); it uses the result retained as the term owner.
ResultTerm.resolve() -> objectobjectReturns the plain str, int or bool value. The owning result must still be open; a result-local symbol ID alone is not a reusable input.
term = result.enumerate_raw("allow", 2)[0][0]
print(term.resolve())Explain a derived fact
After a successful membership check, ask for a witness supporting a derived IDB fact. An explanation does not solve the policy again.
SolveResult.explain_true(
predicate: str,
terms: Sequence[object]
) -> strstrReturns an independent str containing the native MAELYS-DATALOG-v2 document with document=why-true. Inspect its status: it may be complete, truncated or not-derived. Unknown symbol values and native query or capability errors raise MaelysDatalogError.
if result.contains_fact("allow", ["alice", "roadmap.pdf"]):
print(result.explain_true("allow", ["alice", "roadmap.pdf"]))Explain an absent fact
For a successful negative membership answer, inspect the obstacles found by the bounded Why-false exploration.
SolveResult.explain_false(
predicate: str,
terms: Sequence[object]
) -> strstrReturns an independent str containing the native MAELYS-DATALOG-v2 document with document=why-false. Exploration may be truncated; this is not a proof of every possible obstruction. For a fact already present, the document is not-applicable. Unknown symbol values and native query or capability errors raise MaelysDatalogError.
if not result.contains_fact("allow", ["mallory", "roadmap.pdf"]):
print(result.explain_false("allow", ["mallory", "roadmap.pdf"]))Release a solved result
Release the answer once its queries are finished, so an explicit Session can accept another request.
SolveResult.close() -> NoneNoneReturns None after native result release. ResultTerm views can no longer be resolved. A private session created by Ruleset.solve() is also closed; an explicit prepared Session remains reusable.
result.close()Usage and examples
Check one concrete fact, enumerate the answers, then ask why a fact was or was not derived. Configure reusable explanation storage only when you need that optimization.
The reference above gives the exact declarations; the sections below explain their use.
Ground query versus enumeration
A ground query asks one yes/no question — “is
allow("alice", "roadmap.pdf") true?” The Python method
result.contains_fact(predicate, terms) answers it through
maelys_datalog_result_query, without running inference again:
result.contains_fact("allow", ["alice", "roadmap.pdf"]) # True or FalseTerms are plain Python values. String lookup is read-only: an unknown string
in a permitted query returns False without interning or changing it.
The predicate and its query permissions are checked before resolving
terms, so a forbidden or invalid predicate does not become a harmless False
just because one of its strings is also unknown.
- An unknown, wrong-arity, forbidden or non-queryable predicate raises
MaelysDatalogError. - A non-string predicate, a top-level
str/bytesin place of a term sequence, or an unsupported term raises a Python exception. - Old
Term.symbol_id(...)inputs and rawResultTermobjects are not accepted. Pass the symbol's string value, not an ID from another result.
Scope differs from enumeration — the two are not symmetric:
contains_fact()can find query-capable policy facts, runtime EDB facts and derived IDB facts in the retained model;enumerate_predicate_facts()andenumerate_raw()enumerate derived IDB facts only, subject to query permissions and the manifest whitelist when present.
A query-capable policy or EDB fact can therefore make contains_fact() return
True while enumeration has no matching derived row. Neither operation
derives new facts, and an error must never be treated as a negative answer.
contains_fact() is a ground check: every term must be a concrete value.
For a pattern such as “all users allowed to read this document”, enumerate
the predicate and filter the copied tuples in Python:
facts = result.enumerate_predicate_facts("allow", 2)
users = [user for user, doc in facts if doc == "roadmap.pdf"]This Python result filtering is separate from the engine's standard ground filters inside policy source.
Enumerating results: resolved versus raw
As in the original binding, the two forms enumerate the same already-derived IDB facts in the same order; the difference is the last conversion step. The resolved form performs symbol-text lookups and returns one Python value per term, ready to use. Those native lookups have a cost: choose the raw form when you need to inspect kinds or group by result-local identity without resolving every symbol immediately. Neither form reads arbitrary EDB facts.
result.enumerate_predicate_facts(predicate, arity) returns copied Python
tuples. Symbols become strings; integer and boolean values retain their types.
These copies remain usable after closing the result.
result.enumerate_raw(predicate, arity) instead returns tuples of immutable
ResultTerm objects with kind and value. For a symbol, value is a
result-local numeric ID. term.resolve() and
result.resolve_term(term) check that the owning result is still open;
resolution through another result is rejected.
These are not input tokens. No Ruleset.intern_symbol() is provided:
use strings in the EDB and queries, not an ID from another transaction.
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
with Engine() as engine:
engine.register_domain("raw_docs", [
Predicate("user", 1, PRED_EDB),
Predicate("allow", 1, PRED_IDB | PRED_QUERY),
])
ruleset = engine.load_inline_ruleset("raw_docs", "access", "allow(U) :- user(U).")
edb = ruleset.edb()
edb.add_fact("user", ["alice"])
with ruleset.solve(edb) as result:
term = result.enumerate_raw("allow", 1)[0][0]
copied = result.enumerate_predicate_facts("allow", 1)
print(term.kind, term.resolve())
print(copied)
try:
term.resolve()
except RuntimeError:
print("result closed")symbol alice
[('alice',)]
result closedWhy-true text
Once contains_fact() answers that a fact is true, explain_true() asks
the engine why. It returns the canonical witness for an already-derived
IDB fact as a versioned MAELYS-DATALOG-v2 document with
document=why-true, the same Why-true grammar
used by the native engine. The wrapper copies the formatter's UTF-8 text
without stripping or reformatting it.
Without the session-workspace option, the binding prepares one native explanation per method call in
aligned Python-owned storage, reads its cached text length, then writes the
document into a separate output buffer (including room for the NUL terminator).
It releases the prepared handle in finally, even if output allocation,
rendering or UTF-8 decoding fails. There is no second construction to retrieve
the text, and the solver never runs again. This path requires the matching
0.4.0 engine with the caller-owned explanation API; older native
libraries must be rebuilt before using the updated binding.
During preparation and writing, the reference backend performs no internal
heap allocation. Python and CFFI still allocate the workspace, temporary
values and returned string. The method returns an independent str; the
native result is no longer leased by an explanation when the method returns.
Your calls remain explain_true() and explain_false(), with no new handle to
close. On this default path, separate calls prepare separately; there is no cross-call cache. With explanations=, the session owns the cached preparation instead, and result cleanup releases it automatically.
The return type is str, not str | None. This is a deliberate difference
from the original binding's explain_fact_text(). With the reference backend,
keep these cases distinct:
| You asked about | contains_fact() | explain_true() |
|---|---|---|
| A false fact whose symbols are known | False | Text with status=not-derived; not None. |
| A fact true only as EDB or POLICY_FACT | True | Text with status=not-derived: no derived IDB witness. |
| A derived fact with retained provenance | True | Text with status=complete. |
| A derived fact whose witness exceeds the bound | True | Text with status=truncated; the fact is still true. |
| A symbol absent from the retained vocabulary | False for a permitted query | Raises NOT_FOUND, not a fabricated explanation. |
| Invalid/forbidden query or unsupported explanation capability | Raises for invalid queries | Raises MaelysDatalogError; never converted to None. |
status=not-derived is not the same as a false authorization decision:
a fact may be true as EDB or POLICY_FACT but have no derived-IDB provenance.
Conversely, an unknown symbol is a native error for this explanation method,
even though a permitted membership query about it returns False.
Missing explanation support reports UNSUPPORTED. Neither query nor
explanation re-solves the policy.
A complete witness
The program below is executed, byte for byte, by this site's documentation
guard against the matching source checkout — the output block is engine output, not a
transcription. Run it with PYTHONPATH pointing at bindings/python, as in
Loading path:
import maelys_datalog as md
predicates = [
md.Predicate('edge', 2, md.PRED_EDB),
md.Predicate('path', 2, md.PRED_IDB | md.PRED_QUERY),
]
with md.Engine() as engine:
engine.register_domain('why_true_docs', predicates)
ruleset = engine.load_inline_ruleset(
'why_true_docs', 'docs.paths',
'path(X, Y) :- edge(X, Y).\n'
'path(X, Z) :- path(X, Y), edge(Y, Z).',
)
edb = ruleset.edb()
edb.add_fact('edge', ['a', 'b'])
edb.add_fact('edge', ['b', 'c'])
result = ruleset.solve(edb)
assert result.contains_fact('path', ['a', 'c'])
print(result.explain_true('path', ['a', 'c']), end='')MAELYS-DATALOG-v2
document=why-true
status=complete
steps=2 premises=3
step=0 rule=1 fact="path"("a","b")
premise=0 body=0 kind=positive origin=edb fact="edge"("a","b") parent=-
step=1 rule=2 fact="path"("a","c")
premise=1 body=0 kind=positive origin=idb fact="path"("a","b") parent=0
premise=2 body=1 kind=positive origin=edb fact="edge"("b","c") parent=-
result-step=1Each step is one rule application of the witness; each premise cites its
origin (edb or idb) and, for derived premises, the parent step that
produced it. result-step names the step that derived the queried fact.
A truncated witness
Why-true provenance is bounded. When a fact is derived but its witness
does not fit the bound, the engine still answers — with the same canonical
document, status=truncated, and no steps — rather than pretending the fact
has no witness. This program derives wide('a') through 73 rule
applications (1 + 8 + 64), more than the bounded provenance can hold:
import maelys_datalog as md
predicates = [
md.Predicate('base', 1, md.PRED_EDB),
md.Predicate('wide', 1, md.PRED_IDB | md.PRED_QUERY),
]
rules = []
mids = []
for i in range(8):
predicates.append(md.Predicate(f'mid{i}', 1, md.PRED_IDB))
mids.append(f'mid{i}(X)')
leaves = []
for j in range(8):
predicates.append(md.Predicate(f'leaf{i}_{j}', 1, md.PRED_IDB))
rules.append(f'leaf{i}_{j}(X) :- base(X).')
leaves.append(f'leaf{i}_{j}(X)')
rules.append(f'mid{i}(X) :- ' + ', '.join(leaves) + '.')
rules.append('wide(X) :- ' + ', '.join(mids) + '.')
with md.Engine() as engine:
engine.register_domain('why_true_wide', predicates)
ruleset = engine.load_inline_ruleset(
'why_true_wide', 'docs.wide', '\n'.join(rules),
)
edb = ruleset.edb()
edb.add_fact('base', ['a'])
result = ruleset.solve(edb)
assert result.contains_fact('wide', ['a'])
print(result.explain_true('wide', ['a']), end='')MAELYS-DATALOG-v2
document=why-true
status=truncated
steps=0 premises=0contains_fact() still returned True; only the bounded witness is
unavailable. Treat status=truncated as "true, witness too large" — never
as absence.
Why-false text
result.explain_false(predicate, terms) returns the native bounded Why-false
document unchanged. It examines why a ground fact was not derived, using
retained post-solve state. It is not simply the inverse of a Why-true witness.
It uses the same prepare-once, cached-size, write and release lifecycle as
explain_true(), with its own bounded workspace requirements. The Python
method signature, returned document format and exception behavior are unchanged.
| Document status | Meaning |
|---|---|
not-applicable | The queried fact is present. |
complete | Exploration finished without hitting a bound; not a formal absence certificate. |
truncated | A limit was reached; not proof of non-derivability. |
The envelope is MAELYS-DATALOG-v2, followed by document=why-false. It names its obstacles and limit
hits. Reference bounds are 128 candidate rules, 4,096 substitutions per rule,
depth 10 and 16 diagnostics; filter work is also bounded. These limits are
not Python options. Unknown symbols raise NOT_FOUND, and missing capabilities
raise UNSUPPORTED; neither is turned into a false decision.
The prepared-session example demonstrates a contradicted negation. See the Why-false contract for the native format. Never convert a truncated diagnostic into a boolean authorization result.
Reusable explanation workspace (0.5.0)
Opt in when preparing the session, before its first solve. This reserves one native workspace for the selected explanation kinds; no workspace is reserved by default.
from maelys_datalog import ExplanationKind
with ruleset.prepare(
explanations=ExplanationKind.TRUE | ExplanationKind.FALSE,
) as session:
with session.solve(edb) as result:
print(result.explain_true("allow", ["alice", "roadmap.pdf"]))
print(result.explain_false("allow", ["mallory", "roadmap.pdf"]))Use this with the document-access domain and input from the quickstart: Alice is allowed and Mallory is blocked. The method signatures and returned documents are unchanged.
- The reference engine reserves the workspace once at session creation. Each explanation call then measures and writes through that workspace, without a new native allocation or a new CFFI exploration arena.
- The cache holds one explanation. The same result, kind, predicate and typed values reuse preparation; changing the explanation request replaces it. Ordinary membership queries do not evict it, and closing the result clears it.
- A kind omitted from the mask raises
MaelysDatalogErrorwithUNSUPPORTED, not an allocating fallback. - Python still allocates argument conversions, output buffers and strings. This is not a zero-allocation Python binding.
ruleset.solve(edb, explanations=...) accepts the same option, but creates a private session for that result. Reuse an explicit session to amortize workspace creation across requests.
Without the option, the default prepared path is unchanged: it already prepares once per Python method call, but allocates a fresh CFFI workspace for each call. The 0.5.0 improvement for Python is workspace reuse, not removal of a second exploration that the default Python path did not perform.