Python API¶
The synalog package exposes nine functions that take a program (parse, compile, search, compile_all, check, assertions, counterexamples, plan, execute) two that take nothing and return the names Synalog has already reserved (reserved_predicates, builtin_functions), and two that render text a report does not control (quote_value, statement_text). The program functions all accept an optional engine keyword that overrides the program's @Engine annotation (one of sqlite, duckdb, bigquery, psql, presto, trino, databricks; default duckdb) and an optional import_root keyword listing directories where import statements look up .l files (default: the current directory). They raise ValueError on syntax or compilation errors.
parse¶
Parse source and return the AST as a JSON string.
compile¶
Compile a single predicate to SQL.
limit is combined with the @Limit directive: the effective limit is min(limit, @Limit). Use limit/offset for pagination, and make sure every predicate has an @OrderBy so page boundaries are deterministic.
The SQL is one script, for clients that run SQL themselves. A script cannot loop, so a deep recursion is written out step by step, up to the depth @Recursive declares, and a recursion too deep to write out (@Recursive(P, -1), or thousands of steps) raises ValueError. execute runs it instead, stopping when it converges.
search¶
Compile a predicate to SQL that keeps only rows where some column matches the regular expression pattern. Each column is cast to text and matched with the OR of the per-column conditions, using the target engine's native regex operator (~ on PostgreSQL, REGEXP on SQLite, regexp_matches on DuckDB, REGEXP_LIKE elsewhere). This is a real regular expression, not a SQL LIKE pattern. limit/offset apply to the filtered rows.
compile_all¶
Compile every defined predicate in the program. Returns a mapping predicate_name -> sql.
check¶
check(source, engine=None, import_root=None, assertions=True, project=None) -> tuple[list[str], list[str]]
Run structural verification. Returns (errors, warnings), two lists of messages. The program is valid when errors is empty. Warnings do not make it invalid: they report assertions that are well-formed but cannot be checked against a database.
The verifier needs no database. When the program passes it and a database is known, check also runs the program's @Assert statements there: each violated assertion is an error quoting a few counterexamples. A database is known when the program is inside a project whose layer.toml has a [connection]: the one found from project, a folder, or else from the current directory. Otherwise check stays offline.
assertions=Falseskips the database, for callers that need the instant, offline answer.- A database that cannot be reached is a warning (
Assertions not checked: ...), not an error.
errors, warnings = synalog.check(source)
for e in errors:
print("error:", e)
for w in warnings:
print("warning:", w)
assertions¶
Every @Assert of the program and where it stands, in source order. Each dict has the keys predicate, name, statement, status ("pending", "unchecked" or "unsupported") and detail (what a pending assertion waits for, or why an assertion is unsupported).
for assertion in synalog.assertions(source):
print(f"{assertion['predicate']}.{assertion['name']}: {assertion['status']}")
Invalid assertions, such as a statement that does not parse, are reported by check, not here.
counterexamples¶
counterexamples(source, predicate, name, limit=None, offset=None, engine=None, import_root=None) -> str
Compile the search for the counterexamples of an assertion to SQL. The assertion name of predicate holds on a database when the query returns no row there; each row is a counterexample, with one column per variable of the statement's leading ∀ (a ∀ nested in a consequent adds none). limit and offset paginate as in compile.
sql = synalog.counterexamples(source, "Ancestor", "transitive", limit=5)
rows = duckdb.sql(sql).fetchall() # [] when the assertion holds
Raises ValueError if there is no such assertion, or if it is pending or unsupported.
execute¶
execute(source, predicate, engine=None, project=None, import_root=None, limit=None, offset=None, pattern=None, assertion=None, loads=()) -> tuple[list[str], list[tuple]]
Run a predicate on its database and return (columns, rows). Synalog runs the predicate's plan in one connection, so each recursion stops as soon as a step changes nothing: it costs the steps its data needs, whatever the depth @Recursive declares, and @Recursive(P, -1) (until nothing changes) runs on every engine.
- The engine is
engine, else the program's@Engine, else the project's, else duckdb; the connection is the project's[connection]— found fromproject, a folder, or else from the current directory — when it is to that engine. patternkeeps the rows where some column matches it, assearch;limit/offsetpaginate as incompile.assertionreturns the counterexamples of the assertion of that name ofpredicateinstead of its rows.loadsis a sequence of(table, path)pairs: csv/tsv/json/jsonl/parquet files loaded as tables first (duckdb and sqlite).
plan¶
plan(source, predicate, limit=None, offset=None, engine=None, import_root=None, pattern=None, assertion=None) -> list[dict]
The steps that compute a predicate, for a host that runs them itself. Each step is a dict:
| Step | Run it |
|---|---|
{"kind": "setup", "sql": ...} |
the engine's setup: a script |
{"kind": "sql", "sql": ...} |
a statement; the last step's rows are the predicate's |
{"kind": "loop", "body": [...], "repetitions": n, "changed": ...} |
a recursion: run changed, a query returning one number; while it is not 0, run the body statements, n times at most |
All the steps run in one connection: the loops write tables the later steps read. synalog.runners.run_plan(steps, session) runs a plan in a synalog.runners.session.
reserved_predicates¶
The predicate names Synalog defines itself, sorted: the built-in temporal concepts (Today, Now) and every head of every dialect's library program (Num, Str, Epoch, ArgMax, ...). A program may reference these but must not define them.
builtin_functions¶
The function and operator names Synalog compiles to SQL, sorted (Substr, ToString, Like, IsNull, ...). These live in a different namespace from predicates: they appear in call position, not as relations.
Both lists exist for hosts that resolve references themselves. If your rules live in a database rather than in .l files, check alone cannot tell a typo from a predicate defined elsewhere, so you need to know which names are already taken — and which of them are function calls rather than relational references:
reserved = set(synalog.reserved_predicates()) | set(synalog.builtin_functions())
unknown = [name for name in referenced_names(rule) if name not in reserved | defined_in_catalogue]
front_matter¶
The YAML front matter opening a program file, verbatim (without its --- delimiters), or None when the file has none. Synalog checks that the block is well-formed YAML when it parses a file (parse, compile, check report a syntax error on its line), and check reads its name and description (see verification); this function lets a host read the rest of the metadata and agree with the parser on where the block ends. Load the values with your YAML library:
import yaml, synalog
source = open("concepts/ActiveCustomer.l").read()
meta = yaml.safe_load(synalog.front_matter(source) or "") or {}
meta.get("description") # "Customers with at least one delivered order."
quote_value and statement_text¶
How reports show text they do not control. quote_value writes a value from the database as a double-quoted literal, its line breaks, control characters and invisible or reordering characters escaped, at most 200 characters of it; statement_text writes an assertion's statement on one line, the same characters escaped. Reports of violated assertions (check, synalog verify, synalog run) show counterexamples so: text in the data reads as a value, never as part of the report.
Executing the generated SQL¶
execute runs a predicate for you. With compile, execution is up to you, and any driver works: sqlite3, duckdb, psycopg, google-cloud-bigquery, trino, databricks-sql-connector: