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:

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

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

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

terms 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_QUERY predicate raises MaelysDatalogError (even when a string term is also unknown);
  • an explicit Term.symbol_id(n) not valid in this ruleset raises MaelysDatalogError with .code == ERR_INVALID_FIELD — distinct from an unknown string, which is a plain False;
  • a non-string predicate, a top-level str / bytes instead of a term sequence, or an unsupported term value raises TypeError.

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:

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

CODE
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 aboutcontains_fact()explain_fact_text()
A false factFalseNone
A fact true only as EDB or POLICY_FACTTrueNone — Why-true provenance is defined for derived IDB facts
An invalid predicate or symbol idraisesraises MaelysDatalogError / TypeError — an error is never reported as None
A derived fact whose bounded provenance is unavailableTruetext 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:

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_fact_text('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_fact_text('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.