Skip to content

Syntax

Named arguments

Synalog uses named arguments only — there are no positional arguments. In Predicate(column_name: variable), the left side of : is the column name in the predicate, and the right side is your variable name:

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

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

Left side is the column, right side is the variable

Orders(total: amount) does not bind the amount column to total — it looks for a column named total. When in doubt, write the column name on the left.

Variables and expressions

Variables are defined with ==:

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

Operators

Category Operators
Arithmetic + - * / ^ (power) % (modulo)
String concatenation ++
Comparison == != < > <= >=
Boolean && \|\| !
Membership x in [1, 2, 3]
Null tests x is null, x is not null

Never compare against null with !=

x != null is silently broken (it follows SQL three-valued logic and never matches). Always use x is null / x is not null.

Logical operators

Conjunction (AND) — comma , joins predicates:

Result(x:, y:) :- TableA(x:), TableB(x:, y:);

Disjunction (OR) — pipe | combines results (UNION ALL semantics; add distinct to deduplicate):

Combined(x:) distinct :- SourceA(x:) | SourceB(x:);

Negation (NOT) — tilde ~:

Inactive(user_id:) :- Users(user_id:), ~Logins(user_id:);

Multiple rule definitions — defining the same predicate several times unions the bodies:

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

A practical combination — contact customers by email when available, otherwise by phone:

@OrderBy(ContactableCustomer, "customer_id");
ContactableCustomer(customer_id:, channel:) distinct :-
  Customers(customer_id:, email:), email is not null, channel == "email" |
  Customers(customer_id:, phone:), phone is not null, channel == "phone";

Null handling

MissingEmail(user_id:) :- Users(user_id:, email:), email is null;
HasEmail(user_id:, email:) :- Users(user_id:, email:), email is not null;
UserDisplay(user_id:, name:) :- Users(user_id:, full_name:), name == Coalesce(full_name, "Anonymous");

Conditionals

if … then … else expressions, chainable with else if:

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

Records

Build nested record values with {field:, field:}:

UserInfo(user_id:, info:) :- Users(user_id:, name:, email:), info == {name:, email:};

Complete example

Variables, disjunction, negation, conditionals and null handling in one runnable program:

# run: OrderWithTax, ContactableCustomer, Inactive, OrderSize, UserDisplay
@Engine("duckdb");

# Tables
Customers(customer_id: 1, full_name: "Ada Lovelace", email: "ada@example.com", phone: null);
Customers(customer_id: 2, full_name: null, email: null, phone: "555-0102");
Customers(customer_id: 3, full_name: "Grace Hopper", email: "grace@example.com", phone: "555-0103");

Orders(order_id: 10, customer_id: 1, amount: 1500);
Orders(order_id: 11, customer_id: 2, amount: 240);
Orders(order_id: 12, customer_id: 3, amount: 40);

Logins(customer_id: 1);
Logins(customer_id: 3);

# Rules

## Variables are defined with ==.
@OrderBy(OrderWithTax, "order_id");
OrderWithTax(order_id:, total:) :-
  Orders(order_id:, amount:),
  total == amount * 1.10;

## Disjunction (|) with null tests: email when available, otherwise phone.
@OrderBy(ContactableCustomer, "customer_id");
ContactableCustomer(customer_id:, channel:) distinct :-
  Customers(customer_id:, email:), email is not null, channel == "email" |
  Customers(customer_id:, phone:), phone is not null, channel == "phone";

## Negation (~): customers that never logged in.
@OrderBy(Inactive, "customer_id");
Inactive(customer_id:) :- Customers(customer_id:), ~Logins(customer_id:);

## Conditional expression.
@OrderBy(OrderSize, "order_id");
OrderSize(order_id:, size:) :-
  Orders(order_id:, amount:),
  size == (if amount > 1000 then "large"
           else if amount > 100 then "medium"
           else "small");

## Coalesce for null handling.
@OrderBy(UserDisplay, "customer_id");
UserDisplay(customer_id:, name:) :-
  Customers(customer_id:, full_name:),
  name == Coalesce(full_name, "Anonymous");
Generated SQL and execution results
$ synalog.check('syntax.l')
No errors found.

$ synalog.compile('syntax.l', 'OrderWithTax')
-- 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
      10 AS order_id,
      1 AS customer_id,
      1500 AS amount
   UNION ALL

    SELECT
      11 AS order_id,
      2 AS customer_id,
      240 AS amount
   UNION ALL

    SELECT
      12 AS order_id,
      3 AS customer_id,
      40 AS amount

) AS UNUSED_TABLE_NAME  )
SELECT
  Orders.order_id AS order_id,
  ((Orders.amount) * (1.10)) AS total
FROM
  t_0_Orders AS Orders ORDER BY order_id;

-- Executed on DuckDB:
| order_id | total   |
|----------|---------|
| 10       | 1650.00 |
| 11       | 264.00  |
| 12       | 44.00   |
(3 rows)

$ synalog.compile('syntax.l', 'ContactableCustomer')
-- 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_1_Customers AS (SELECT * FROM (

    SELECT
      1 AS customer_id,
      E'Ada Lovelace' AS full_name,
      E'ada@example.com' AS email,
      null AS phone
   UNION ALL

    SELECT
      2 AS customer_id,
      null AS full_name,
      null AS email,
      E'555-0102' AS phone
   UNION ALL

    SELECT
      3 AS customer_id,
      E'Grace Hopper' AS full_name,
      E'grace@example.com' AS email,
      E'555-0103' AS phone

) AS UNUSED_TABLE_NAME  ),
t_0_ContactableCustomer_MultBodyAggAux AS (SELECT * FROM (

    SELECT
      Customers.customer_id AS customer_id,
      E'email' AS channel
    FROM
      t_1_Customers AS Customers
    WHERE
      (Customers.email IS NOT null)
   UNION ALL

    SELECT
      t_2_Customers.customer_id AS customer_id,
      E'phone' AS channel
    FROM
      t_1_Customers AS t_2_Customers
    WHERE
      (t_2_Customers.phone IS NOT null)

) AS UNUSED_TABLE_NAME  )
SELECT
  ContactableCustomer_MultBodyAggAux.customer_id AS customer_id,
  ContactableCustomer_MultBodyAggAux.channel AS channel
FROM
  t_0_ContactableCustomer_MultBodyAggAux AS ContactableCustomer_MultBodyAggAux
GROUP BY ContactableCustomer_MultBodyAggAux.customer_id, ContactableCustomer_MultBodyAggAux.channel ORDER BY customer_id;

-- Executed on DuckDB:
| customer_id | channel |
|-------------|---------|
| 1           | email   |
| 2           | phone   |
| 3           | email   |
| 3           | phone   |
(4 rows)

$ synalog.compile('syntax.l', 'Inactive')
-- 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_Customers AS (SELECT * FROM (

    SELECT
      1 AS customer_id,
      E'Ada Lovelace' AS full_name,
      E'ada@example.com' AS email,
      null AS phone
   UNION ALL

    SELECT
      2 AS customer_id,
      null AS full_name,
      null AS email,
      E'555-0102' AS phone
   UNION ALL

    SELECT
      3 AS customer_id,
      E'Grace Hopper' AS full_name,
      E'grace@example.com' AS email,
      E'555-0103' AS phone

) AS UNUSED_TABLE_NAME  ),
t_1_Logins AS (SELECT * FROM (

    SELECT
      1 AS customer_id
   UNION ALL

    SELECT
      3 AS customer_id

) AS UNUSED_TABLE_NAME  )
SELECT
  Customers.customer_id AS customer_id
FROM
  t_0_Customers AS Customers
WHERE
  ((SELECT
    MIN((CASE WHEN x_4.unnested_pod = 0 THEN 1 ELSE NULL END)) AS logica_value
  FROM
    t_1_Logins AS Logins, (select unnest([0]) as unnested_pod) as x_4
  WHERE
    (Logins.customer_id = Customers.customer_id)) IS NULL) ORDER BY customer_id;

-- Executed on DuckDB:
| customer_id |
|-------------|
| 2           |
(1 row)

$ synalog.compile('syntax.l', 'OrderSize')
-- 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
      10 AS order_id,
      1 AS customer_id,
      1500 AS amount
   UNION ALL

    SELECT
      11 AS order_id,
      2 AS customer_id,
      240 AS amount
   UNION ALL

    SELECT
      12 AS order_id,
      3 AS customer_id,
      40 AS amount

) AS UNUSED_TABLE_NAME  )
SELECT
  Orders.order_id AS order_id,
  CASE WHEN (Orders.amount > 1000) THEN E'large' WHEN (Orders.amount > 100) THEN E'medium' ELSE E'small' END AS size
FROM
  t_0_Orders AS Orders ORDER BY order_id;

-- Executed on DuckDB:
| order_id | size   |
|----------|--------|
| 10       | large  |
| 11       | medium |
| 12       | small  |
(3 rows)

$ synalog.compile('syntax.l', 'UserDisplay')
-- 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_Customers AS (SELECT * FROM (

    SELECT
      1 AS customer_id,
      E'Ada Lovelace' AS full_name,
      E'ada@example.com' AS email,
      null AS phone
   UNION ALL

    SELECT
      2 AS customer_id,
      null AS full_name,
      null AS email,
      E'555-0102' AS phone
   UNION ALL

    SELECT
      3 AS customer_id,
      E'Grace Hopper' AS full_name,
      E'grace@example.com' AS email,
      E'555-0103' AS phone

) AS UNUSED_TABLE_NAME  )
SELECT
  Customers.customer_id AS customer_id,
  COALESCE(Customers.full_name, E'Anonymous') AS name
FROM
  t_0_Customers AS Customers ORDER BY customer_id;

-- Executed on DuckDB:
| customer_id | name         |
|-------------|--------------|
| 1           | Ada Lovelace |
| 2           | Anonymous    |
| 3           | Grace Hopper |
(3 rows)