Skip to content

Directives

Directives control predicate behavior. They must be placed before the rule definition they apply to.

@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 an iteration limit. See Recursion.
@Ground(Pred) Force materialization before dependents (performance).
@Engine(name) Target SQL engine. See Supported engines.
@Assert(Pred, name: "statement") State a property the predicate must satisfy. See Assertions.

@OrderBy

@OrderBy(Stats, "category");
@OrderBy(TopCustomers, "total", "DESC");

Each item is a column of the predicate, optionally followed by ASC or DESC and by NULLS FIRST or NULLS LAST (in any case): "total DESC", "name", "score desc nulls last". Nulls come last in both directions unless NULLS FIRST says otherwise, text is ordered by code point ("B" before "a", "Z" before "a") and booleans false before true, on every engine. Rows equal on every item come in any order: end the list with a column that tells them apart ("total DESC", "customer_id") for pages to be stable. Anything else, such as an expression ("x * 2"), is refused by both the verifier and the compiler, since the item is written into the SQL's ORDER BY. To order by a computed value, compute it in a column of the rule.

@OrderBy is mandatory in practice

Put @OrderBy on every concept and rule. Without a stable sort order, pagination (limit/offset in compile()) returns rows in a non-deterministic order between calls.

@Limit

@Limit(TopCustomers, 10);

The limit is a whole number of rows, 0 or more (0 gives no row). It is part of what the predicate holds: a rule that uses TopCustomers sees only its 10 rows, and so do its assertions.

@Limit combines with the limit argument of compile(): the effective limit is min(limit, @Limit).

@Recursive

Enables recursion on a predicate, with a maximum number of iterations:

@Recursive(AllManagers, 20);

It takes the predicate and the number of iterations, nothing else. The number of iterations is optional (8 by default), a whole number, 1 or more, or -1: until a step changes nothing. It bounds the recursion; Synalog stops earlier when the recursion converges. See how recursion runs.

@Ground

Forces a predicate to be materialized before its dependents are evaluated, useful when a predicate is reused by many rules and recomputing it inline would be wasteful:

@Ground(CustomerRevenue);

The table is named after the predicate, in Synalog's schema (CustomerRevenue); a predicate named after an SQL keyword gets _table added (Order_table), since Order does not parse as a table name.

@Engine

Selects the target SQL dialect for the whole program:

@Engine("duckdb");

The engine keyword of the Python API functions overrides this annotation.

@Assert

States a property of a predicate in first-order logic. Each property has a name, given as a named argument:

@Assert(Ancestor, transitive: "∀ x y z, Ancestor x y → Ancestor y z → Ancestor x z");

@Recursive(Ancestor, 20);
Ancestor(x:, y:) :- Parent(x:, y:);
Ancestor(x:, y: z) :- Ancestor(x:, y:), Parent(x: y, y: z);

Unlike the other directives, it does not change the generated SQL, and it can be written before its predicate exists. Assertions are checked against a database with verify. See Assertions.

Complete example

@OrderBy and @Limit combined: the top 3 customers by total spend:

# run: TopCustomers
@Engine("duckdb");

Orders(customer_id: 100, amount: 250);
Orders(customer_id: 100, amount: 1200);
Orders(customer_id: 200, amount: 80);
Orders(customer_id: 300, amount: 430);
Orders(customer_id: 400, amount: 990);

# Directives go BEFORE the rule they apply to.
@OrderBy(TopCustomers, "total", "DESC");
@Limit(TopCustomers, 3);
TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, amount:);
Generated SQL and execution results
$ synalog.check('directives.l')
No errors found.

$ synalog.compile('directives.l', 'TopCustomers')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;

WITH t_0_Orders AS (SELECT * FROM (

    SELECT
      100 AS customer_id,
      250 AS amount
   UNION ALL

    SELECT
      100 AS customer_id,
      1200 AS amount
   UNION ALL

    SELECT
      200 AS customer_id,
      80 AS amount
   UNION ALL

    SELECT
      300 AS customer_id,
      430 AS amount
   UNION ALL

    SELECT
      400 AS customer_id,
      990 AS amount

) AS UNUSED_TABLE_NAME  )
SELECT
  Orders.customer_id AS customer_id,
  SUM(Orders.amount) AS total
FROM
  t_0_Orders AS Orders
GROUP BY Orders.customer_id ORDER BY total DESC LIMIT 3;

-- Executed on DuckDB:
| customer_id | total |
|-------------|-------|
| 100         | 1450  |
| 400         | 990   |
| 300         | 430   |
(3 rows)