Python

Runtime EDB

Edb in the unified Python binding.

The Python runtime EDB reference continues the Python overview.

Classes

ClassKindRole
EdbResource-owning classOwns the mutable input facts for one complete request.

Methods

MethodPurpose
Ruleset.edb()Create an input buffer
Edb.add_fact()Append one fact
Edb.add_facts()Append an atomic batch
Edb.clear()Discard an unsolved batch
Edb.reset()Start a new request in the same buffer
Edb.close()Release input storage

Properties and attributes

Use Python's length protocol to inspect the buffer. It is not an additional native count method; the Edb card below explains its ownership.

Property / protocolTypeMeaning
len(edb)intNumber of stored entries before deduplication; Python length protocol, not an Edb.count() method.

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.

Edb reference

Python · class
Edb
Creation
Ruleset.edb(
    *,
    fact_capacity: int | None = None,
    text_capacity: int | None = None,
) -> Edb
Native input buffer normally created with ruleset.edb(). Edb(ruleset, fact_capacity=..., text_capacity=...) is also supported. len(edb) counts entries before deduplication. Close explicitly or through its Ruleset owner; Edb is not a context manager.
Methods
Example
edb = ruleset.edb()
edb.add_fact("owns", ["alice", "roadmap.pdf"])
print(len(edb))

Creating an Edb

Obtain this object through the following method. Do not call its internal handle-taking constructor.

OperationPurpose
Ruleset.edb()Create an input buffer

Edb attributes and protocols

Use Python’s len(edb) protocol to count stored entries. It is not an Edb.count() method.

Attribute / protocolTypeMeaning
len(edb)intNumber of stored entries before deduplication; Python length protocol, not an Edb.count() method.

Create an input buffer

Keep request facts separate from the compiled policy. Optional capacities reserve bounded input storage; they do not change solver limits.

Python · method
Ruleset.edb
Ruleset.edb(
*,
fact_capacity: int | None = None,
text_capacity: int | None = None
) -> Edb
Creates an empty input buffer associated with this ruleset. Fill it with runtime facts before solving a request.
Arguments
fact_capacityint | None
Maximum stored input entries, before deduplication. None selects engine.limits.max_edb_facts.
text_capacityint | None
Native text-storage budget in bytes. None selects engine.limits.input_edb_text_bytes.
Return value
Edb

Returns a new, empty Edb associated with this Ruleset. Invalid capacities raise TypeError or ValueError; native creation failures raise MaelysDatalogError.

Example
edb = ruleset.edb()
edb.add_fact("owns", ["alice", "roadmap.pdf"])

Append one fact

This copies one fact into the input buffer. It does not run rules or validate the complete policy vocabulary.

Python · method
Edb.add_fact
Edb.add_fact(
predicate: str,
terms: Sequence[object]
) -> None
Appends one typed input fact, copying its text values into native storage. This call does not evaluate the policy.
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
None

Returns None after a successful copy. Python shape errors and native storage errors raise exceptions; earlier successful additions remain. Exact predicate vocabulary is checked at solve.

Example
edb.add_fact("owns", ["alice", "roadmap.pdf"])

Append an atomic batch

Prefer a batch when several facts form one logical addition: either all are appended or none are.

Python · method
Edb.add_facts
Edb.add_facts(
facts: Iterable[tuple[str, Sequence[object]]]
) -> None
Appends a batch of input facts atomically: if validation or storage fails, none of this batch is added. Earlier successful additions are retained.
Arguments
factsIterable[tuple[str, Sequence[object]]]
Iterable of (predicate, terms) pairs. The complete addition is atomic; previously stored facts remain on failure.
Return value
None

Returns None after appending the whole batch. Python validation, generator or native storage failure appends none of this batch. Earlier additions remain; generator side effects are not rolled back.

Example
edb.add_facts([
    ("user", ["alice"]),
    ("owns", ["alice", "roadmap.pdf"]),
    ("blocked", ["mallory"]),
])

Discard an unsolved batch

Clear input after correcting an invalid unsolved request. This reuses the same native storage.

Python · method
Edb.clear
Edb.clear() -> None
Empties an input buffer that has not been successfully solved. Use reset() instead when starting a new request after a successful solve.
Arguments
Return value
None

Returns None after emptying an unsolved Edb. A successfully solved Edb is frozen: clear() then raises RuntimeError; use reset() to start a new request.

Example
edb.clear()
edb.add_fact("owns", ["alice", "corrected.pdf"])

Start a new request in the same buffer

Reset explicitly ends the mutation freeze after successful solving and removes all input facts. It does not release an existing result.

Python · method
Edb.reset
Edb.reset() -> None
Starts a new, empty input batch in the same buffer, including after a successful solve. It does not release the session’s previous result.
Arguments
Return value
None

Returns None with an empty, mutable Edb. The session still rejects a second solve while its previous result remains open. Native failures raise MaelysDatalogError.

Example
with session.solve(edb) as result:
    print(result.contains_fact("allow", ["alice", "roadmap.pdf"]))
edb.reset()
edb.add_fact("owns", ["bob", "notes.txt"])
with session.solve(edb) as result:
    print(result.contains_fact("allow", ["bob", "notes.txt"]))

Release input storage

Once solving has produced a retained result, the input buffer is no longer required to query that result.

Python · method
Edb.close
Edb.close() -> None
Releases native input storage. Results already produced from this buffer remain valid.
Arguments
Return value
None

Returns None after releasing native input storage. Already returned results remain valid. Repeated closure is harmless; using the closed Edb raises RuntimeError.

Example
edb.close()

Usage and examples

Build a request with plain Python values, understand batch insertion and validation, then reuse its storage. Input values and raw output identifiers have different roles.

The reference above gives the exact declarations; the sections below explain their use.

EDB: adding facts

ruleset.edb() creates an independent opaque C input buffer, owned by Python. edb.add_fact(predicate, terms) immediately copies one predicate name and its terms into native storage. edb.add_facts(iterable) temporarily stages pairs, then appends them atomically in C. Neither leaves a persistent Python fact list. len(edb) counts entries before deduplication; edb.clear() empties an unsolved buffer. Ruleset and Engine closure free any remaining EDB handles.

EDB facts take Python values directly: strings, signed 64-bit integers and booleans, mixed freely within a term sequence. The old binding exported InputTerm, ResolvedTerm, Fact, RawFact and a writable Term helper. Do not copy those imports into Python: inputs here are plain Python values, resolved outputs are tuples of those values, and raw output terms use the separate, result-owned ResultTerm class.

Python valueCopied at addition asNotes
strMAELYS_DATALOG_VALUE_SYMBOLUTF-8 text supplied to the native batch; no ruleset.intern_symbol call.
intMAELYS_DATALOG_VALUE_INTEGERFull signed int64 range; values outside that range raise OverflowError.
boolMAELYS_DATALOG_VALUE_BOOLEANEncoded before the int case: True is a boolean, not integer 1.
float / legacy Term / ResultTermNot accepted as inputUse plain str, int or bool. ResultTerm is an output view, not an input token.

The example needs its own vocabulary: declaring owns does not implicitly declare quota_exceeded or has_flag.

CODE
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY

with Engine() as engine:
    engine.register_domain("typed_next_docs", [
        Predicate("owns", 2, PRED_EDB),
        Predicate("quota_exceeded", 2, PRED_EDB),
        Predicate("has_flag", 2, PRED_EDB),
        Predicate("allow", 2, PRED_IDB | PRED_QUERY),
    ])
    ruleset = engine.load_inline_ruleset(
        "typed_next_docs", "typed.main",
        "allow(User, Document) :- owns(User, Document).",
    )
    edb = ruleset.edb()
    edb.add_fact("owns", ["alice", "roadmap.pdf"])      # str + str
    edb.add_fact("quota_exceeded", ["alice", 1024])     # str + int
    edb.add_fact("has_flag", ["alice", True])          # str + bool
    with ruleset.solve(edb) as result:
        print(result.enumerate_predicate_facts("allow", 2))
CODE
[('alice', 'roadmap.pdf')]

Unit and batch: the same choice in C and Python

The following snippets assume a live input EDB and a domain declaring user/1, owns/2 and blocked/1. C also uses the status rc, the diagnostic object and a cleanup path. Choose the unit or batch example; do not execute both to build the same request.

CODEUnit fact addition
rc = MAELYS_DATALOG_ADD_FACT(
    edb, &diagnostic, "owns", "alice", "roadmap.pdf");
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
CODEAtomic fact batch
rc = MAELYS_DATALOG_ADD_FACTS(
    edb, &diagnostic,
    MAELYS_DATALOG_FACT("user", "alice"),
    MAELYS_DATALOG_FACT("owns", "alice", "roadmap.pdf"),
    MAELYS_DATALOG_FACT("blocked", "mallory")
);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;

A batch either appends all its facts or none. Existing facts are preserved on failure. By contrast, three independent unit calls can leave two facts stored if the third call fails. The guarantee concerns input insertion, not solving: undeclared predicates and policy constraints can still fail later.

Python uses ordinary methods, not C macros. C11's FACT and ADD_FACTS conveniences do not change the ABI or the native batch API. See C input facts, with and without macros for conversions, lifetimes and the explicit typed-array form for dynamic input.

Buffered additions, one native batch

  1. Add facts
    add_fact / add_facts
  2. Native input EDB
    owned copies; bounded storage
  3. solve(edb)
    evaluate the entire native buffer
  4. Result
    query without re-solving

Both addition methods copy values into the opaque native EDB. add_facts() accepts an iterable of (predicate, terms) pairs, including a generator. Its temporary Python staging area is bounded by the native entry limit; no Python fact list remains after return. Later changes to the caller's term lists cannot alter the owned native copies.

Each add_facts() call is atomic for Python-side and native storage errors: a malformed pair, invalid term, failing generator, exhausted entry capacity, overlong string or allocation failure appends none of that batch. Earlier successful additions remain. Side effects performed by a caller's generator are not rolled back. Python checks types, signed 64-bit bounds and embedded NULs; no floats or pre-interned Term objects are accepted.

Policy-specific checks — predicate existence, declared arity and kind, per-predicate and shared symbol-pool capacities — occur at solve(). A rejected solve produces no partial result. Use edb.clear() to discard an invalid unsolved input, then rebuild and retry. This is full-batch evaluation, not incremental inference.

WhenWhat is checked
AdditionPython representation; native copy, entry count, arity ceiling and per-string byte bound.
SolveSelected policy vocabulary, exact arity, predicate kind, symbol pool and per-predicate capacities.
Successful solvePython freezes the EDB for mutation; the C buffer API itself remains reusable.

The C facade exposes maelys_datalog_input_edb_t and maelys_datalog_session_solve_edb() through <maelys/datalog.h> alone. These calls are included in 0.4.0: build the CFFI extension and native library from that same release.

Native errors at solve time

This standalone example deliberately buffers a fact with the wrong arity:

CODE
from maelys_datalog import (
    Engine, Predicate, MaelysDatalogError, PRED_EDB, PRED_IDB, PRED_QUERY,
)

with Engine() as engine:
    engine.register_domain("invalid_batch_docs", [
        Predicate("owns", 2, PRED_EDB),
        Predicate("allow", 2, PRED_IDB | PRED_QUERY),
    ])
    ruleset = engine.load_inline_ruleset(
        "invalid_batch_docs", "batch.main",
        "allow(User, Doc) :- owns(User, Doc).",
    )
    edb = ruleset.edb()
    edb.add_fact("owns", ["alice", "doc.pdf"])
    edb.add_fact("owns", ["bob", "doc.pdf", "extra"])  # Buffered, not yet rejected.
    try:
        ruleset.solve(edb)
    except MaelysDatalogError as error:
        print(error.message)
    else:
        raise AssertionError("The invalid batch must not succeed.")
CODE
Invalid fact at index 1: owns expects 2 arguments, received 3.

Indices are zero-based across the complete EDB buffer, not just the last addition. Use error.code, error.message and error.hint for diagnosis; message wording is not a stable machine-readable grammar. A Python TypeError or ValueError is distinct from MaelysDatalogError, which can arise during native addition or solving. Append diagnostics index the submitted batch; solve diagnostics index the complete EDB.

Reuse an input buffer

After a successful solve, clear() is rejected because that EDB is frozen. Use reset() to empty and reuse its native storage for a new complete request; this does not release a live result or make a Session accept a second result until the first is closed.

Inserting by pre-interned symbol ID

The original binding lets a caller intern a string once and reuse its ruleset-local integer handle. Python deliberately does not carry that input optimization across the opaque boundary. Submit the string itself, even if it appears in several buffered facts; the native batch prepares its symbols. A Python integer always means an integer term, never an implicit symbol identifier.

Raw output still exists, but its IDs belong to one retained result. They are not portable between results or reusable in later input batches. This is a scope change, not the loss of the ability to supply symbols. See resolved versus raw enumeration.