Metadata-Version: 2.4
Name: synalog
Version: 2.0.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Compilers
Requires-Dist: click>=8.1
Requires-Dist: rich>=13
Requires-Dist: duckdb
Requires-Dist: psycopg[binary]>=3.1 ; platform_python_implementation == 'CPython' and platform_machine in 'x86_64 AMD64 arm64 aarch64'
Requires-Dist: psycopg>=3.1 ; platform_python_implementation != 'CPython' or platform_machine not in 'x86_64 AMD64 arm64 aarch64'
Requires-Dist: tomli>=1.1 ; python_full_version < '3.11'
Requires-Dist: trino ; extra == 'all-engines'
Requires-Dist: presto-python-client ; extra == 'all-engines'
Requires-Dist: databricks-sql-connector ; extra == 'all-engines'
Requires-Dist: google-cloud-bigquery ; extra == 'all-engines'
Requires-Dist: google-cloud-bigquery ; extra == 'bigquery'
Requires-Dist: databricks-sql-connector ; extra == 'databricks'
Requires-Dist: presto-python-client ; extra == 'presto'
Requires-Dist: trino ; extra == 'trino'
Provides-Extra: all-engines
Provides-Extra: bigquery
Provides-Extra: databricks
Provides-Extra: presto
Provides-Extra: trino
License-File: LICENSE
License-File: NOTICE
Summary: Logic programming for AI agents: Datalog-family language compiling to optimized SQL
Author: Synalinks
License: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://github.com/synalinks/synalog

<div align="center">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/synalinks/synalog/main/img/synalog-dark.svg">
  <img height="200" alt="Synalog" src="https://raw.githubusercontent.com/synalinks/synalog/main/img/synalog-light.svg">
</picture>
</div>

<div align="center">

![Beta](https://img.shields.io/badge/Release-Beta-blue.svg)
[![PyPI](https://img.shields.io/pypi/v/synalog?logo=pypi&logoColor=white&cacheSeconds=86400)](https://pypi.org/project/synalog/)
[![Downloads](https://static.pepy.tech/badge/synalog)](https://pepy.tech/project/synalog)
[![Discord](https://img.shields.io/discord/1118241178723291219?logo=discord&logoColor=white&label=Discord&cacheSeconds=3600)](https://discord.gg/82nt97uXcM)
[![CI](https://github.com/synalinks/synalog/actions/workflows/CI.yml/badge.svg)](https://github.com/synalinks/synalog/actions/workflows/CI.yml)
[![Documentation](https://github.com/synalinks/synalog/actions/workflows/docs.yml/badge.svg)](https://github.com/synalinks/synalog/actions/workflows/docs.yml)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-green.svg)](https://opensource.org/license/apache-2-0)

</div>

# Synalog
## Logic programming for AI agents: Datalog-family language compiling to optimized SQL

Synalog is a logic programming language from the [Datalog](https://en.wikipedia.org/wiki/Datalog) family, a fork of [Logica](https://logica.dev/) with the entire engine (parser, compiler and verifier) **rewritten in Rust**. It compiles to optimized **SQL** and ships as a Python package built on [PyO3](https://pyo3.rs/): parsing is **~86.7x faster** and compilation **~13.7x faster** than the original Python implementation, so validating and compiling a program is effectively instant.

Synalog was built for the AI agents era. The main idea is to give an agent a **dynamic semantic layer** over its data: a layer of named concepts and rules that the agent both reads from *and writes to* at inference time. Unlike a traditional BI semantic layer, which is modeled once by humans and frozen, Synalog's layer is authored on the fly: the agent extracts entities and relationships into **knowledge graphs**, derives meaning with composable **logical rules**, and reasons over time with **temporal reasoning**: accumulating all of it as structured, reusable memory.

New here, or deciding whether this belongs in your stack? [Why Synalog](https://synalinks.github.io/synalog/why/) makes the case in business terms, including the trade-offs and the cases where another tool wins.

## What the agent gains

A raw table is just rows; an agent has to re-interpret what they *mean* on every query. Synalog turns that meaning into a layer the agent owns:

- **A shared vocabulary over raw tables**: instead of re-deriving "who is an active customer" or "what counts as revenue" in every query, the agent defines it once as a named concept and reuses it everywhere. The semantic layer is the agent's interface to the data.
- **Instant knowledge-graph construction from raw tables**: a handful of entity and relationship concepts turn existing relational tables into a traversable knowledge graph, with no ETL pipeline, no separate graph database, and no data movement. The graph is *virtual*: it compiles to SQL that runs directly on the source tables.
- **Knowledge graphs the agent can traverse**: model entities and relationships as concepts, then follow connections (composition, inverse, symmetric, recursive chains) without writing fragile join logic. See [Knowledge graphs](https://synalinks.github.io/synalog/knowledge-graphs/).
- **Recursion and transitive reasoning**: transitive closures and graph traversals (org charts, taxonomies, bills of materials, referral chains, shortest paths) that are impossible to write correctly in raw SQL come out as a base case plus a recursive case, with the verifier guaranteeing termination.
- **Logical rules that compose**: rules build on other rules, so knowledge accumulates instead of being re-derived. Complex questions decompose into small named predicates the agent can inspect, reuse, and combine.
- **Temporal reasoning**: time-aware rules and edges (validity windows, "active today", overlap, point-in-time joins) let the agent answer *when*, not just *what*. That kind of reasoning is notoriously error-prone to express directly in SQL.
- **Dynamic, not static**: the layer evolves as the agent learns. New rules extend the vocabulary at runtime; the rule base itself becomes the agent's long-term memory over structured data.
- **Auditable reasoning**: every derived fact traces back through named rules, giving full lineage from answer to source tables.
- **Compile-time verification**: a formal verifier catches structural errors before any SQL touches a database, so a self-authored rule that parses but is unsound is rejected up front. See [Verification](https://synalinks.github.io/synalog/verification/).
- **A fast Rust engine**: check and compile sit inside the agent's inner loop (every generated rule is validated, every query compiled, often several times per step). The Rust core makes that loop cost milliseconds instead of seconds. See [Benchmark](https://synalinks.github.io/synalog/benchmark/).

Because SQL engines are better optimized than logic programming engines, Synalog compiles into *optimized SQL* that runs on **SQLite**, **DuckDB**, **BigQuery**, **PostgreSQL**, **Presto**, **Trino** and **Databricks**: efficiently scaling to *petabytes of data*. See [Supported engines](https://synalinks.github.io/synalog/engines/).

Full documentation: **<https://synalinks.github.io/synalog/>**

## At a glance

```python
import duckdb
import synalog

source = """
@Engine("duckdb");

Employee(name: "Alice", department: "Engineering", salary: 75000);
Employee(name: "Bob", department: "Marketing", salary: 65000);
Employee(name: "Charlie", department: "Engineering", salary: 80000);

@OrderBy(EngineeringTeam, "name");
EngineeringTeam(name:, salary:) :- Employee(name:, department: "Engineering", salary:);
"""

errors, warnings = synalog.check(source)
assert errors == []

sql = synalog.compile(source, "EngineeringTeam")
rows = duckdb.sql(sql).fetchall()
# [('Alice', 75000), ('Charlie', 80000)]
```

## Installation

```bash
pip install synalog
```

Or with [uv](https://docs.astral.sh/uv/): `uv add synalog` (or `uv pip install synalog`). The CLI also runs without installing via `uvx synalog`.

Requires Python 3.10+. Wheels are published for Linux (x86_64, aarch64, armv7, s390x, ppc64le; glibc and musl), Windows (x64, x86, aarch64) and macOS (x86_64, aarch64).

## Add the skill to your coding agent

Synalog ships an [Agent Skill](https://agentskills.io), a `SKILL.md` that teaches a coding agent the language, the CLI and the conventions, so it writes and runs programs correctly. It follows the open Agent Skills standard, so it works with Claude Code, Cursor, Codex, OpenCode, Cline, Windsurf and 70+ other agents. Install it with the [`skills`](https://www.npmjs.com/package/skills) CLI (GitHub is the registry, so there is nothing to publish or install first):

```bash
npx skills add SynaLinks/synalog              # this project (./.claude/skills, ./.agents/skills, …)
npx skills add SynaLinks/synalog -g           # user-wide (~/.claude/skills, …)
npx skills add SynaLinks/synalog -a cursor codex   # only specific agents
```

The skill is maintained in this repo at [`skills/synalog/SKILL.md`](https://github.com/synalinks/synalog/blob/main/skills/synalog/SKILL.md); `npx skills add` copies it into the right place for each agent.

## Command-line interface

Installing the package also installs a `synalog` command (also available as `python -m synalog`):

```bash
synalog program.l print Predicate      # print compiled SQL
synalog program.l run Predicate        # execute and print a table
synalog program.l run Predicate --csv  # execute and print CSV
```

Both `print` and `run` validate the whole program first and exit 1 with the verifier's errors if it is invalid, so verification always happens before any SQL is produced or executed.

```
$ synalog program.l run EngineeringTeam
+---------+--------+
| name    | salary |
+---------+--------+
| Alice   | 75000  |
| Charlie | 80000  |
+---------+--------+
2 rows
```

- `-` as the file reads the program from stdin; `-c PROGRAM` passes the program text inline, like `python -c`.
- `--engine` overrides the program's `@Engine` annotation (default `duckdb`).
- `--limit` / `--offset` paginate the result.
- `--csv` (with `run`) prints results as CSV instead of the rendered table.
- `--search REGEX` (with `print`/`run`) keeps only rows where some column matches the regular expression `REGEX`, e.g. `synalog program.l run Customers --search "(?i)acme"`. In the interactive session the same is `.search Customers (?i)acme`.
- `run` executes on `duckdb` (the default), `sqlite` (stdlib) or `psql` (with `--dsn` or `SYNALOG_PSQL_DSN`) out of the box: synalog depends on duckdb and on psycopg with its bundled libpq, on macOS and Linux alike. For other engines, install their driver (`pip install 'synalog[trino]'`, ...) or use `print` and run the SQL with your own client.
- `import path.to.file.Pred;` statements resolve `path/to/file.l` against the program file's directory, then the current directory; pass `--import-root DIR` (repeatable) to search elsewhere.
- `--load TABLE=PATH` (repeatable) loads a csv/tsv/json/jsonl/parquet file as a table before running, e.g. `synalog senior.l run Senior --load employees=employees.csv`.

### Projects: `synalog.toml`

A folder with a `synalog.toml` is a project. Its `[connection]` says which database its programs run on, as plain fields — commit it:

```toml
[connection]
engine = "psql"
host = "db.example.com"
port = 5432
database = "sales"
user = "analyst"
schema = "public"
```

Secrets never go in the file (synalog refuses it): each comes from the environment as `SYNALOG_<ENGINE>_<FIELD>` — `SYNALOG_PSQL_PASSWORD`, `SYNALOG_DATABRICKS_ACCESS_TOKEN`, or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery — usually from the project's `.env`, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), `run`, `print` and `verify` use that engine and connection, imports resolve from the project's folder, and `synalog introspect` writes the project's `tables/`, one file per table of the database; `--engine`, an `@Engine` annotation, `--dsn` and `SYNALOG_<ENGINE>_DSN` still take precedence. The fields of every engine are in `synalog.project.ENGINES`.

Running `synalog` with no arguments starts an interactive session, in the spirit of `python` (the options above, e.g. `--engine` or `--load`, apply to it too):

```
$ synalog
Synalog 0.1.0 on duckdb, type .help for help
>>> Employee(name: "Alice", salary: 75000);
>>> Employee(name: "Bob", salary: 65000);
>>> Total(t? += salary) distinct :- Employee(salary:);
>>> Total
+--------+
| t      |
+--------+
| 140000 |
+--------+
1 row
```

Type a rule ending in `;` to add it to the session program (it is validated first, and rejected with an error if invalid), or a predicate name to compile and run it. `.help` lists the session commands (`.show`, `.sql <Pred>`, `.engine <name>`, `.load <table> <path>`, `.clear`, `.exit`).

## Quick start

```python
import synalog

source = """
@Engine("duckdb");

Employee(name: "Alice", department: "Engineering", salary: 75000);
Employee(name: "Bob", department: "Marketing", salary: 65000);
Employee(name: "Charlie", department: "Engineering", salary: 80000);

@OrderBy(EngineeringTeam, "name");
EngineeringTeam(name:, salary:) :- Employee(name:, department: "Engineering", salary:);
"""

errors, warnings = synalog.check(source)
assert errors == []

sql = synalog.compile(source, "EngineeringTeam")
print(sql)
```

You can then execute the SQL with any database driver (`sqlite3`, `duckdb`, `psycopg`, `google-cloud-bigquery`, etc.).

## Python API

### `parse(source, file_name=None, engine=None, import_root=None) -> str`

Parse source and return the AST as a JSON string.

```python
ast = synalog.parse(source)
```

### `compile(source, predicate, limit=None, offset=None, engine=None, import_root=None) -> str`

Compile a single predicate to SQL. `limit` is combined with the `@Limit` directive: `actual = min(limit, @Limit)`.

```python
sql = synalog.compile(source, "TopCustomers", limit=20, offset=40)
```

### `search(source, predicate, pattern, limit=None, offset=None, engine=None, import_root=None) -> str`

Compile a predicate to SQL that keeps only rows where **some column matches the regular expression `pattern`** (the per-column conditions are OR-ed, each column cast to text). The regex is evaluated by the target engine's native operator (`~` on PostgreSQL, `REGEXP` on SQLite, `regexp_matches` on DuckDB, `REGEXP_LIKE` elsewhere). It is *not* a SQL `LIKE` pattern. `limit`/`offset` apply to the filtered rows.

```python
sql = synalog.search(source, "Customers", "(?i)acme", limit=20)
```

### `compile_all(source, engine=None, import_root=None) -> dict[str, str]`

Compile every defined predicate in the program. Returns a mapping `predicate_name -> sql`.

```python
sqls = synalog.compile_all(source)
for name, sql in sqls.items():
    print(name, sql)
```

### `execute(source, predicate, engine=None, dsn=None, import_root=None, limit=None, offset=None, pattern=None, assertion=None, loads=()) -> tuple[list[str], list[tuple]]`

Run a predicate on its database (the program's or project's engine and connection) and return `(columns, rows)`. Each recursion stops as soon as a step changes nothing, so it costs the steps its data needs, and `@Recursive(P, -1)` (until nothing changes) runs on every engine. `plan()` returns the same steps for a host that runs them itself.

```python
columns, rows = synalog.execute(source, "TopCustomers", limit=20)
```

### `check(source, engine=None, import_root=None, assertions=True, dsn=None) -> tuple[list[str], list[str]]`

Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose `synalog.toml` has a `[connection]` (or given a `dsn`), it also runs the program's `@Assert` statements against the database and reports each violated one as an error; `assertions=False` keeps it offline.

```python
errors, warnings = synalog.check(source)
if errors:
    for e in errors:
        print(e)
```

All of these functions 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.

## Language overview

A Synalog program is a **project**: a folder with a `synalog.toml`, holding one predicate per file in three folders, the layout of [semantic layers](https://github.com/SynaLinks/semantic-layers):

```
shop/
├── tables/Orders.l               # the data: one file per database table
├── concepts/Customer.l           # entities and relationships extracted from tables
├── rules/CustomerRevenue.l       # insights derived from concepts
├── rules/TopCustomers.l
└── synalog.toml                  # the project's name, description and database
```

Each file is named after its predicate and opens with front matter (`name`, the predicate; `description`, what its rows are), then imports what it builds on by folder, file and predicate, then orders its predicate with `@OrderBy`. A database table is referenced by its lowercase database name in `tables/`, and everything else builds on the PascalCase predicate that maps it:

`tables/Orders.l`

```logica
---
name: Orders
description: One row per order, with its customer, amount and status.
---
@OrderBy(Orders, "order_id");
Orders(order_id:, customer_id:, amount:, status:) :-
  orders(order_id:, customer_id:, amount:, status:);
```

`concepts/Customer.l`

```logica
---
name: Customer
description: Every customer who placed at least one order.
---
import tables.Orders.Orders;

@OrderBy(Customer, "customer_id");
Customer(customer_id:) distinct :- Orders(customer_id:);
```

`rules/CustomerRevenue.l`

```logica
---
name: CustomerRevenue
description: Total amount ordered by each customer, all statuses included.
---
import concepts.Customer.Customer;
import tables.Orders.Orders;

@OrderBy(CustomerRevenue, "customer_id");
CustomerRevenue(customer_id:, total? += amount) distinct :-
  Customer(customer_id:), Orders(customer_id:, amount:);
```

Imports resolve from the project's folder, so every file runs on its own: `synalog rules/CustomerRevenue.l run CustomerRevenue`. The snippets below show the language itself, one rule at a time.

### Named arguments

Synalog uses **named arguments only** (no positional arguments). The left side of `:` is the column name, the right side is the variable:

```logica
# column "amount" bound to variable "total"
Orders(amount: total)

# shorthand: column and variable share the same name
Orders(amount:)
```

### Variables and expressions

Variables are defined with `==`. Arithmetic, string and comparison operators are supported:

```logica
OrderWithTax(order_id:, total:) :-
  Orders(order_id:, amount:),
  total == amount * 1.10;
```

**Operators**: `+`, `-`, `*`, `/`, `^` (power), `%` (modulo), `++` (string concat), `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`, `in`, `is null`, `is not null`.

### Aggregation

Aggregation uses the `distinct` keyword and special operators in the rule head:

```logica
# Sum
Revenue(total? += amount) distinct :- Orders(amount:);

# Count
OrderCount(n? += 1) distinct :- Orders(order_id:);

# Min / Max
Cheapest(min_price? Min= price) distinct :- Products(price:);
Priciest(max_price? Max= price) distinct :- Products(price:);

# Average
AvgOrder(avg? Avg= amount) distinct :- Orders(amount:);

# Collect into list / set
AllNames(names? List= name) distinct :- Users(name:);
UniqueNames(names? Set= name) distinct :- Users(name:);

# Value with max/min key
TopSeller(name? ArgMax= name -> revenue) distinct :- Sales(name:, revenue:);
```

### Logical operators

```logica
# Conjunction (AND), comma
Result(x:, y:) :- TableA(x:), TableB(x:, y:);

# Disjunction (OR), pipe
Combined(x:) distinct :- SourceA(x:) | SourceB(x:);

# Negation (NOT), tilde
Inactive(user_id:) :- Users(user_id:), ~Logins(user_id:);
```

Multiple rule definitions for the same predicate combine results (union):

```logica
HighValue(user_id:) :- Orders(user_id:, amount:), amount > 10000;
HighValue(user_id:) :- Referrals(user_id:, tier: "vip");
```

### Conditionals

```logica
OrderSize(order_id:, size:) :-
  Orders(order_id:, amount:),
  size == (if amount > 1000 then "large"
           else if amount > 100 then "medium"
           else "small");
```

### Directives

Directives control predicate behavior and **must be placed before** the rule definition:

```logica
@OrderBy(TopCustomers, "total", "DESC");
@Limit(TopCustomers, 10);
TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, amount:);
```

| Directive | Purpose |
|-----------|---------|
| `@OrderBy(Pred, col1, ...)` | Sort order. Append `"DESC"` for descending |
| `@Limit(Pred, n)` | Maximum number of rows |
| `@Recursive(Pred, n)` | Allow recursion with iteration limit |
| `@Ground(Pred)` | Force materialization before dependents |
| `@Engine(name)` | Target SQL engine |

### Functors

Functors let you reuse predicate logic by parameterizing input predicates:

```logica
# Define a reusable pattern
@OrderBy(SegmentRevenue, "segment_id");
SegmentRevenue(segment_id:, total? += amount) distinct :-
  Segment(segment_id:, user_id:),
  Orders(user_id:, amount:);

# Apply to different segments
EnterpriseRevenue := SegmentRevenue(Segment: EnterpriseCustomers);
SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
```

### Recursion

Recursive predicates compute transitive closures, for example finding all managers above an employee:

```logica
@Recursive(AllManagers, 20);

# Base case: direct manager
AllManagers(employee_id:, manager_id:) :- Employees(employee_id:, manager_id:);

# Recursive case: manager's managers
AllManagers(employee_id:, manager_id:) :-
  AllManagers(employee_id:, manager_id: intermediate),
  Employees(employee_id: intermediate, manager_id:);
```

Useful for: referral chains, org charts, product taxonomies, bill of materials.

### Shortest paths

Find shortest paths in weighted graphs by enumerating route costs recursively, then keeping the minimum per destination with a `Min=` aggregation:

```logica
# Enumerate route costs from the origin, hop by hop.
@Recursive(RouteCost, 10);
RouteCost(destination:, cost:) :-
  ShippingRoutes(origin: "warehouse_main", destination:, cost:);
RouteCost(destination:, cost: total) :-
  RouteCost(destination: hub, cost: hub_cost),
  ShippingRoutes(origin: hub, destination:, cost:),
  total == hub_cost + cost;

# Keep the cheapest cost per destination.
@OrderBy(ShippingCost, "destination");
ShippingCost(destination:, total? Min= cost) distinct :- RouteCost(destination:, cost:);
```

### Temporal data

When working with timestamps or dates, always convert to string first:

```logica
@OrderBy(MonthlyOrders, "month");
MonthlyOrders(month:, count? += 1) distinct :-
  Orders(created_at:),
  month == Substr(ToString(created_at), 1, 7);

# Filter by date range
RecentOrders(order_id:) :-
  Orders(order_id:, created_at:),
  ToString(created_at) >= "2024-01-01";
```

`Today(date:)` (today's date as `"YYYY-MM-DD"`) and `Now(timestamp:)` (the current instant as the engine's native timestamp) are built-in concepts. They are inlined per dialect by the compiler, with no runtime table needed, so they work on every engine. `Now` is the most precise value; derive coarser parts (date, time, hour) from it through the `ToString` → `Substr` pipeline. Join against `Today` whenever a rule needs "today":

```logica
@OrderBy(ThisMonthOrders, "order_id");
ThisMonthOrders(order_id:, created_at:) :-
  Orders(order_id:, created_at:),
  Today(date:),
  Substr(ToString(created_at), 1, 7) == Substr(date, 1, 7);
```

They are reserved names: you cannot redefine, extend, or update them.

### Knowledge graphs

Entities and relationships become node and edge concepts, and rules traverse them. Edges join **through** the node concepts rather than the raw table, so a filter on a node applies to every edge built on it:

```logica
@OrderBy(Person, "person_id");
Person(person_id:, name:, profile_url:) distinct :- Employees(person_id:, name:, profile_url:);

@OrderBy(WorksIn, "person_id");
WorksIn(person_id:, department_id:) distinct :-
  Person(person_id:),
  Department(department_id:),
  Employees(person_id:, department_id:);
```

Relationships with a lifetime carry it as a half-open interval `[valid_from, valid_to)`, using `"9999-12-31"` as the open end so ISO strings compare correctly, and `Today` supplies the clock for point-in-time questions:

```logica
@OrderBy(CurrentEmployment, "person_id");
CurrentEmployment(person_id:, company_id:, role:) distinct :-
  EmployedAt(person_id:, company_id:, role:, valid_from:, valid_to:),
  Today(date:),
  valid_from <= date, date < valid_to;
```

Node and edge patterns, temporal modeling, point-in-time queries and time-respecting traversals are covered in [Knowledge graphs](https://synalinks.github.io/synalog/knowledge-graphs/).

### Built-in functions

**String:** `Substr`, `Length`, `Upper`, `Lower`, `Split`, `Join`, `Like`, `Format`

**Array:** `Size`, `Element`, `ArrayConcat`, `Range`

**Math:** `Abs`, `Floor`, `Ceil`, `Round`, `Sqrt`, `Log`, `Exp`, `Sin`, `Cos`

**Type casting:** `ToInt64`, `ToFloat64`, `ToString`

**Other:** `Coalesce`, `IsNull`

**Built-in concepts:** `Today(date:)` is today's date as `"YYYY-MM-DD"`; `Now(timestamp:)` is the current instant as a native timestamp (see Temporal data above).

## Supported engines

| Engine | `@Engine` value | Notes |
|--------|-----------------|-------|
| DuckDB | `duckdb` | Default engine |
| SQLite | `sqlite` | |
| PostgreSQL | `psql` | |
| BigQuery | `bigquery` | |
| Trino | `trino` | |
| Presto | `presto` | |
| Databricks | `databricks` | Double-quoted string literals |

Each engine has its own SQL dialect for string literals, array syntax, GROUP BY style, record construction, regex matching, and standard library functions.

## Benchmark

The Rust core is benchmarked against the original Python Logica implementation on every program of the compiler test suite (504 programs across 6 engines). Both run **in-process**: Synalog through the same PyO3 extension that `pip install synalog` ships, so the numbers measure exactly what a Python caller gets:

| | Python Logica | Synalog (Rust) | Speedup |
|---|---|---|---|
| Parse | 13.4 s | 0.15 s | **87x** |
| Compile | 61.3 s | 5.2 s | **13x** |
| Verify | n/a | 0.16 s | Rust-only |

<p align="center">
  <img src="https://raw.githubusercontent.com/synalinks/synalog/main/docs/benchmark/speedup_by_engine.png" alt="Synalog vs Python Logica: speedup by SQL engine" width="820">
</p>

Speedup is the geometric mean of per-program speedups (every program weighted equally). Parsing is uniformly ~85x to ~88x faster; compilation ranges from ~11x (trino, presto) to ~19x (duckdb). Verification (safety, stratification, recursion and reserved-name checks) is a Synalog-specific pass with no standalone Python equivalent. Per-engine tables and methodology are on the [Benchmark](https://synalinks.github.io/synalog/benchmark/) page; reproduce with `python3 benchmark.py`.

## Verification

Unlike Logica, which lets the database raise errors at execution time, Synalog embeds a **formal verifier** that catches issues at compile time.

| Check | What it detects |
|-------|-----------------|
| **Safety** | Head variables not bound in the body |
| **Safe negation** | Negated variables without a positive occurrence |
| **Safe aggregation** | Aggregated variables not bound outside the aggregate |
| **Stratification** | Negative recursion cycles |
| **Arity** | Predicates used with inconsistent argument counts |
| **Recursion** | Missing base cases, trivial loops, unbounded recursion without `@Recursive` |
| **Reserved names** | Rules that redefine a built-in library predicate (`Num`, `Str`, `ArgMin`, `Today`, `Now`, ...) |
| **Unsafe `SqlExpr`** | User rules that reach for the raw-SQL escape hatch |

```python
errors, warnings = synalog.check(bad_source)
for e in errors:
    print(e)
# Unbound variable 'y' in head of rule: Test(x:, y:) :- Numbers(x:)
```

## Differences with Datalog and Logica

### Named attributes only

Synalog doesn't support positional attributes like Logica or Datalog: it only uses *named attributes*, which reduce agent mistakes. This feature is optional in Logica; we made it mandatory.

In Synalog, the compiled SQL uses actual column names, not `col{i}` format, making it compatible with existing database schemas.

### Pagination

Pagination is critical for AI agents with limited context windows. It also avoids loading large amounts of data into memory, enabling use on memory-constrained cloud infrastructure.

Synalog applies pagination at compile time via the `limit` and `offset` arguments of `compile()`. The limit is combined with the `@Limit` directive: `actual_limit = min(limit, @Limit)`.

### Compile-time verification

Synalog embeds a formal verifier that catches structural errors before any SQL is generated. This prevents agents from producing programs that parse correctly but fail at execution time, a common failure mode when working with SQL directly.

## Building from source

The project uses [maturin](https://www.maturin.rs/) to build the Python wheel from the Rust crate.

```bash
pip install maturin
maturin develop --release    # install into the active venv
maturin build --release      # produce a wheel in target/wheels/
```

Run the Rust test suite:

```bash
cargo test
```

Run specific test groups:

```bash
cargo test --lib                        # unit tests
cargo test --test compiler_tests        # compiler golden tests (all engines)
cargo test --test parser_tests          # parser golden tests (all engines)
cargo test --test verifier_tests        # verifier tests (all engines)
cargo test --test search_tests          # search feature tests (all engines)
```

### Tests

```shell
cd synalog
./shell/test.sh
```

# Credits

Synalog is a fork of [**Logica**](https://logica.dev/), the logic programming language created by **Evgeny Skvortsov** and the Logica team at **Google LLC**. The parser, compiler, and type-inference engine were rewritten in Rust, but the language design, semantics, and a large part of the algorithms originate from their work. Synalog would not exist without it, heartfelt thanks to the Logica authors for their amazing work.

- Logica: https://github.com/EvgSkv/logica
- Logica documentation: https://logica.dev/

Files derived from Logica carry an `// Original work: Copyright 2020 Google LLC` header and a note describing the modifications, as required by the Apache License 2.0.

# License

Synalog is licensed under the **Apache License, Version 2.0** see [`LICENSE`](https://github.com/synalinks/synalog/blob/main/LICENSE) and [`NOTICE`](https://github.com/synalinks/synalog/blob/main/NOTICE).

- Original Logica work: Copyright 2020 Google LLC, licensed under Apache 2.0.
- Modifications and new code: Copyright 2025-2026 Yoan Sallami (Synalinks Team), licensed under Apache 2.0.


