Python V1 · Deprecated
Querying
Querying in the Python V1 binding.The Python V1 querying reference continues the Python V1 overview.
Enumerating results (resolved vs raw)
SolveResult.enumerate_predicate_facts resolves symbol terms to strings
automatically — each resolution is one native cffi call, cheap but not
free. Each returned tuple mixes resolved strings, native int, and bool,
one Python object per term, ready to use without a decode step.
The WASM binding can enumerate
the same derived IDB facts with enumerate(predicate, arity), returning
copied rows of strings, bigint integers and booleans. Raw symbol IDs are no
longer exposed by the JavaScript surface.
enumerate_predicate_facts_raw returns the same facts without that resolution
step. The difference is in what each tuple holds:
resolved: list[Fact] = result.enumerate_predicate_facts('allow', 2)
# [('alice', 'roadmap.pdf')] -> plain str / int / bool, ready to use
raw: list[RawFact] = result.enumerate_predicate_facts_raw('allow', 2)
# [(Term.symbol_id(7), Term.symbol_id(12))] -> opaque, ruleset-local integer handlesBoth methods enumerate the same already-derived facts in the same order; they
differ only in the final conversion. The resolved form is what you usually
want. Reach for the raw form to skip the per-term resolution cost, or to
compare and group facts by identity — but those integer handles belong to the
originating Ruleset: they are neither stable across rulesets nor portable
identifiers.
Ground query vs enumeration
A ground query asks one yes/no question — "is allow("alice", "roadmap.pdf") true?" SolveResult.contains_fact(predicate, terms) answers it
directly, the same capability C exposes as
maelys_datalog_query_solved_ground_fact and WASM as
query:
result.contains_fact('allow', ['alice', 'roadmap.pdf']) # -> True or Falseterms is a Sequence[InputTerm]. String terms are resolved read-only
under the owning Ruleset lock — an unknown string returns False without
interning or mutating the symbol table. The predicate is validated before any
term is resolved, so an invalid predicate raises rather than masquerading as
a False:
- a forbidden, unknown, wrong-arity, or non-
PRED_QUERYpredicate raisesMaelysDatalogError(even when a string term is also unknown); - an explicit
Term.symbol_id(n)not valid in this ruleset raisesMaelysDatalogErrorwith.code == ERR_INVALID_FIELD— distinct from an unknown string, which is a plainFalse; - a non-string
predicate, a top-levelstr/bytesinstead of a term sequence, or an unsupported term value raisesTypeError.
Scope differs from enumeration — the two are not symmetric:
contains_fact()searches query-capable policy facts, the finalized EDB snapshot, and derived IDB facts;enumerate_predicate_facts()(and its raw variant) enumerate derived IDB facts only.
So a query-capable policy or EDB fact can make contains_fact() return True
while enumerate_predicate_facts() returns no matching row. Neither API derives
new facts.
contains_fact is a ground check — every term must be a concrete value. For
a pattern query with a variable ("all X such that allow(X, "roadmap.pdf")"), enumerate the predicate and filter on the Python side:
facts = result.enumerate_predicate_facts('allow', 2)
users = [user for user, doc in facts if doc == 'roadmap.pdf']Why-true text
Once contains_fact() answers that a fact is true,
SolveResult.explain_fact_text(predicate, terms) asks the engine why: it
returns the canonical witness for an already-derived IDB fact, as the
opaque versioned MAELYS-DATALOG-v2 document with the document=why-true
discriminator — the same Why-true document
grammar the WASM binding exposes through
explainTrue. The wrapper returns
the C formatter's UTF-8 bytes without stripping or reformatting them, and
neither contains_fact() nor explain_fact_text() derives new facts.
explanation = result.explain_fact_text('path', ['a', 'c'])
if explanation is not None:
print(explanation, end='')The return type is str | None, and four results must not be confused —
only two of them are None:
| You asked about | contains_fact() | explain_fact_text() |
|---|---|---|
| A false fact | False | None |
| A fact true only as EDB or POLICY_FACT | True | None — Why-true provenance is defined for derived IDB facts |
| An invalid predicate or symbol id | raises | raises MaelysDatalogError / TypeError — an error is never reported as None |
| A derived fact whose bounded provenance is unavailable | True | text with status=truncated — truncation is never collapsed to None |
None means only that no matching derived IDB fact is available. A
query-capable POLICY_FACT or EDB fact can make contains_fact() return
True while explain_fact_text() returns None — the same asymmetry as
enumeration, for the same reason. Term resolution also mirrors
contains_fact(): string terms use the read-only lookup under the owning
Ruleset lock, so an unknown string returns None without interning it,
while an invalid predicate or explicit symbol id raises before any term is
resolved.
A complete witness
The program below is executed, byte for byte, by this site's documentation
guard against the pinned engine — 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_fact_text('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_fact_text('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.