Python

Querying

Querying in the unified Python binding.

The Python querying reference continues the Python overview.

Classes

ClassKindRole
SolveResultResource-owning classRetains a solved model for queries, enumeration and explanations.
ResultTermImmutable value classImmutable raw term tied to its live result.

Methods

MethodPurpose
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 / protocolTypeMeaning
result.fingerprintstrSelected-policy authority.
result.execution_fingerprintstrSelected policy plus execution configuration.
term.kindstrsymbol, integer or boolean.
term.valueint | boolResult-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

Python · class
SolveResult
Creation
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
Solved answer returned by session.solve() or ruleset.solve(); do not construct it from native handles. Read-only fingerprint and execution_fingerprint are str properties. Queries and raw term resolution require this result to remain open.
Methods
Example
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.

OperationPurpose
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 / protocolTypeMeaning
result.fingerprintstrSelected-policy authority.
result.execution_fingerprintstrSelected policy plus execution configuration.

ResultTerm reference

Python · class
ResultTerm
@dataclass(frozen=True)
class ResultTerm:
    kind: str
    value: int | bool
    _owner: SolveResult = field(repr=False)
Immutable raw output value returned by enumerate_raw(). It retains its result owner for resolution; do not manufacture it as input. Symbol IDs are scoped to that result, not the Ruleset.
Fields
kindstr
Term kind: symbol, integer or boolean.
valueint | bool
Result-local numeric symbol ID, signed integer, or boolean according to kind.
_ownerSolveResult
Internal owning SolveResult retained for resolution; not a user input field.
Example
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.

Python · method
SolveResult.contains_fact
SolveResult.contains_fact(
predicate: str,
terms: Sequence[object]
) -> bool
Checks whether one fully specified fact is present in the existing result. It does not solve the policy again.
Arguments
predicatestr
Nonempty predicate name declared by the selected policy; the operation also checks its arity and permissions.
termsSequence[object]
Concrete str, signed 64-bit int or bool values in predicate order. A string alone is not a term sequence.
Return value
bool

Returns 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.

Example
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.

Python · method
SolveResult.derived_fact_count
SolveResult.derived_fact_count() -> int
Reads how many facts were derived during this solve, without enumerating the model or deriving more facts.
Arguments
Return value
int

Returns an int. It does not derive more facts or enumerate the model. A closed result raises RuntimeError; native failures raise MaelysDatalogError.

Example
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.

Python · method
SolveResult.enumerate_predicate_facts
SolveResult.enumerate_predicate_facts(
predicate: str,
arity: int
) -> list[tuple[object, ...]]
Lists a query-enabled derived relation as ordinary Python values copied from the result.
Arguments
predicatestr
Nonempty predicate name declared by the selected policy; the operation also checks its arity and permissions.
arityint
Number of terms declared for the predicate. Supply an int, not a bool.
Return value
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.

Example
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.

Python · method
SolveResult.enumerate_raw
SolveResult.enumerate_raw(
predicate: str,
arity: int
) -> list[tuple[ResultTerm, ...]]
Lists a derived relation as typed ResultTerm records, keeping symbols as IDs local to this live result.
Arguments
predicatestr
Nonempty predicate name declared by the selected policy; the operation also checks its arity and permissions.
arityint
Number of terms declared for the predicate. Supply an int, not a bool.
Return value
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.

Example
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.

Python · method
SolveResult.resolve_term
SolveResult.resolve_term(
term: ResultTerm
) -> object
Converts a raw term from this result into its ordinary Python string, integer or boolean value.
Arguments
termResultTerm
ResultTerm produced by this same, still-open result; a term from another result is rejected.
Return value
object

Returns 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.

Example
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.

Python · method
ResultTerm.resolve
ResultTerm.resolve() -> object
Resolves this raw term through its owning result to obtain the corresponding Python value.
Arguments
Return value
object

Returns 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.

Example
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.

Python · method
SolveResult.explain_true
SolveResult.explain_true(
predicate: str,
terms: Sequence[object]
) -> str
Explains a derivation of the requested fact as a bounded Why-true document, using the existing solve result.
Arguments
predicatestr
Nonempty predicate name declared by the selected policy; the operation also checks its arity and permissions.
termsSequence[object]
Concrete str, signed 64-bit int or bool values in predicate order. A string alone is not a term sequence.
Return value
str

Returns 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.

Example
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.

Python · method
SolveResult.explain_false
SolveResult.explain_false(
predicate: str,
terms: Sequence[object]
) -> str
Investigates obstacles to deriving an absent fact and produces a bounded Why-false document. It is not an exhaustive proof of all possible obstructions.
Arguments
predicatestr
Nonempty predicate name declared by the selected policy; the operation also checks its arity and permissions.
termsSequence[object]
Concrete str, signed 64-bit int or bool values in predicate order. A string alone is not a term sequence.
Return value
str

Returns 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.

Example
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.

Python · method
SolveResult.close
SolveResult.close() -> None
Releases this result, including any private session created by Ruleset.solve(). An explicitly prepared session remains available for another request.
Arguments
Return value
None

Returns 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.

Example
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:

CODE
result.contains_fact("allow", ["alice", "roadmap.pdf"])  # True or False

Terms 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/bytes in place of a term sequence, or an unsupported term raises a Python exception.
  • Old Term.symbol_id(...) inputs and raw ResultTerm objects 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() and enumerate_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:

CODE
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.

CODE
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")
CODE
symbol alice
[('alice',)]
result closed

Why-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 aboutcontains_fact()explain_true()
A false fact whose symbols are knownFalseText with status=not-derived; not None.
A fact true only as EDB or POLICY_FACTTrueText with status=not-derived: no derived IDB witness.
A derived fact with retained provenanceTrueText with status=complete.
A derived fact whose witness exceeds the boundTrueText with status=truncated; the fact is still true.
A symbol absent from the retained vocabularyFalse for a permitted queryRaises NOT_FOUND, not a fabricated explanation.
Invalid/forbidden query or unsupported explanation capabilityRaises for invalid queriesRaises 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:

CODE
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='')
CODE
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=1

Each 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:

CODE
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='')
CODE
MAELYS-DATALOG-v2
document=why-true
status=truncated
steps=0 premises=0

contains_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 statusMeaning
not-applicableThe queried fact is present.
completeExploration finished without hitting a bound; not a formal absence certificate.
truncatedA 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.

CODE
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 MaelysDatalogError with UNSUPPORTED, 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.