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 ==:
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:
Disjunction (OR) — pipe | combines results (UNION ALL semantics; add distinct to deduplicate):
Negation (NOT) — tilde ~:
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:}:
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)