Skip to content

SQL Goal Compiler

This notebook demonstrates how to translate a TypedLogic theory plus a query or goal into SQL.

The examples use an in-memory SQLite database so the generated SQL is executed end to end.

Setup

The SQL compiler is a regular TypedLogic compiler. PredicateBinding tells the compiler which SQL table and columns back an extensional predicate.

import sqlite3

from typedlogic import And, Forall, Implies, NegationAsFailure, Not, PredicateDefinition, SentenceGroup, Term, Theory, Variable
from typedlogic.compilers.sql_compiler import PredicateBinding, SQLCompiler, SQLCompilerConfig
from typedlogic.datamodel import SentenceGroupType


def fetchall(sql: str, setup: list[str] | None = None) -> list[tuple]:
    """Run SQL in a fresh in-memory SQLite database."""
    connection = sqlite3.connect(":memory:")
    try:
        for statement in setup or []:
            connection.execute(statement)
        return list(connection.execute(sql))
    finally:
        connection.close()


compiler = SQLCompiler()

Rules As Views

This theory defines Ancestor from Parent. The recursive rule is compiled as a recursive common table expression.

x = Variable("x")
y = Variable("y")
z = Variable("z")

family_theory = Theory(
    predicate_definitions=[
        PredicateDefinition("Parent", {"parent": "str", "child": "str"}),
        PredicateDefinition("Ancestor", {"ancestor": "str", "descendant": "str"}),
    ]
)
family_theory.add(Forall([x, y], Implies(Term("Parent", x, y), Term("Ancestor", x, y))))
family_theory.add(
    Forall(
        [x, y, z],
        Implies(And(Term("Ancestor", x, y), Term("Parent", y, z)), Term("Ancestor", x, z)),
    )
)

ancestor_sql = compiler.compile(
    family_theory,
    goal=Term("Ancestor", "Alice", Variable("who")),
    bindings={"Parent": PredicateBinding("parent_edges", {"parent": "src", "child": "dst"})},
)
print(ancestor_sql)
fetchall(
    ancestor_sql,
    [
        "CREATE TABLE parent_edges (src TEXT, dst TEXT)",
        "INSERT INTO parent_edges VALUES ('Alice', 'Bob'), ('Bob', 'Carol'), ('Carol', 'Dana')",
    ],
)

Inline Unit Clauses

Ground facts do not require table bindings. They become inline CTE branches.

person_theory = Theory(predicate_definitions=[PredicateDefinition("Person", {"name": "str"})])
person_theory.add(Term("Person", "Alice"))
person_theory.add(Term("Person", "Bob"))

person_sql = compiler.compile(person_theory, goal=Term("Person", Variable("name")))
print(person_sql)
fetchall(person_sql)

Goal Sentence Groups

If no explicit goal is supplied, the compiler uses goal sentence groups from the theory.

goal_theory = Theory(predicate_definitions=[PredicateDefinition("Person", {"name": "str"})])
goal_theory.add(Term("Person", "Alice"))
goal_theory.sentence_groups.append(
    SentenceGroup(name="goals", group_type=SentenceGroupType.GOAL, sentences=[Term("Person", Variable("name"))])
)

goal_sql = compiler.compile(goal_theory)
print(goal_sql)
fetchall(goal_sql.removesuffix(";"))

Negation As Failure

Negated body atoms are emitted as correlated NOT EXISTS subqueries.

eligibility_theory = Theory(
    predicate_definitions=[
        PredicateDefinition("Person", {"name": "str"}),
        PredicateDefinition("Blocked", {"name": "str"}),
        PredicateDefinition("Eligible", {"name": "str"}),
    ]
)
eligibility_theory.add(
    Forall([x], Implies(And(Term("Person", x), NegationAsFailure(Term("Blocked", x))), Term("Eligible", x)))
)

eligible_sql = compiler.compile(
    eligibility_theory,
    goal=Term("Eligible", Variable("name")),
    bindings={"Person": "people", "Blocked": "blocked"},
)
print(eligible_sql)
fetchall(
    eligible_sql,
    [
        "CREATE TABLE people (name TEXT)",
        "CREATE TABLE blocked (name TEXT)",
        "INSERT INTO people VALUES ('Alice'), ('Bob')",
        "INSERT INTO blocked VALUES ('Bob')",
    ],
)

Constraint Checks

First-order denial constraints compile to validation queries. A returned row is a violation witness.

constraint_theory = Theory(
    predicate_definitions=[
        PredicateDefinition("Cat", {"name": "str"}),
        PredicateDefinition("Dog", {"name": "str"}),
    ]
)
constraint_theory.add(Forall([x], Not(And(Term("Cat", x), Term("Dog", x)))))

[constraint_sql] = compiler.compile_constraints(
    constraint_theory,
    config=SQLCompilerConfig(bindings={"Cat": PredicateBinding("cats"), "Dog": PredicateBinding("dogs")}),
)
print(constraint_sql)
fetchall(
    constraint_sql,
    [
        "CREATE TABLE cats (name TEXT)",
        "CREATE TABLE dogs (name TEXT)",
        "INSERT INTO cats VALUES ('Jules'), ('Milo')",
        "INSERT INTO dogs VALUES ('Milo'), ('Rex')",
    ],
)