-
Notifications
You must be signed in to change notification settings - Fork 8
Tutorial 07 Expressions
← Section 6: Aggregates | → Section 8: Prepared queries
The Corestore team needs to answer questions that go beyond pattern matching: which products are affordable, what did an order line item actually cost, which deliveries arrived late, and which SKUs follow the expected naming convention? These questions call for expression clauses — inline computations and predicates that run inside a :where block after the pattern clauses have bound their variables.
Expression clauses come in two forms. A filter keeps only the bindings where its expression is truthy:
[(< ?price 500)]
A binding evaluates its expression and assigns the result to a new variable that can be used in later clauses or in :find:
[(* ?qty ?price) ?line-total]
Both forms share one hard rule: every ?variable that appears inside the expression must already have been bound by an earlier :where clause. Minigraf checks this statically — using an unbound variable is a parse error, not a silent no-result.
Cumulative state at the start of this section: tx_count = 17.
Two short transacts add fields needed for the examples in this section.
; tx 18: add unit price to Ben's laptop line item
; (:order-item/qty 1 was already asserted in tx 6 — asserting it again would
; create a second fact with a different valid-from, causing duplicate query rows)
(transact [
[:ben-order-1-item-1 :order-item/price 1299]
])
; tx 19: add integer epoch values to Clara's order for delay arithmetic
(transact [
[:clara-order-1 :order/promise-epoch 20229]
[:clara-order-1 :order/actual-epoch 20234]
])
20229 and 20234 are days since the Unix epoch. May 20 2026 is day 20229; May 25 2026 is day 20234 — five days late.
After tx 19, tx_count = 19.
The most common expression clause is a comparison filter. Place a single predicate expression inside double brackets and Minigraf will discard any binding row where it is false.
(query [:find ?name ?price
:where [?p :product/name ?name]
[?p :product/price ?price]
[(< ?price 500)]])
?name ?price
----------------------------------------
"ClearView 27" Monitor" 449
"NoiseCancel Pro" 249
"Compact Keyboard" 89
"USB-C Cable 2m" 19
4 result(s) found.
All six comparison operators work the same way. They can compare a variable against a literal or compare two variables against each other:
[(< ?price 500)] ; less than
[(>= ?price 100)] ; greater than or equal
[(= ?status :placed)] ; equality — works on keywords too
[(!= ?sku "LP-15")] ; not equal
[(< ?low ?high)] ; two-variable comparison
[(>= ?actual ?promise)] ; both sides are bound variables
A binding expression evaluates an arithmetic expression and assigns the result to a new variable. Write the expression first and the target variable second, both inside the outer brackets:
(query [:find ?product-name ?qty ?price ?line-total
:where [:ben-order-1-item-1 :order-item/product ?p]
[?p :product/name ?product-name]
[:ben-order-1-item-1 :order-item/qty ?qty]
[:ben-order-1-item-1 :order-item/price ?price]
[(* ?qty ?price) ?line-total]])
?product-name ?qty ?price ?line-total
------------------------------------------
"LaptopPro 15" 1 1299 1299
1 result(s) found.
[(* ?qty ?price) ?line-total] evaluates ?qty × ?price and binds the result to ?line-total. Once bound, ?line-total is a regular variable: it can appear in subsequent expression clauses (for example, as input to another arithmetic step) or in :find as shown here.
Arithmetic operators: + - * /. Integer division truncates toward zero. Division by zero silently drops the row rather than raising an error.
The delivery-delay query shows the two forms working together. The binding computes the delay; the filter discards on-time orders:
(query [:find ?customer-name ?delay-days
:where [?order :order/customer ?customer]
[?customer :customer/name ?customer-name]
[?order :order/promise-epoch ?promise]
[?order :order/actual-epoch ?actual]
[(- ?actual ?promise) ?delay-days]
[(> ?delay-days 0)]])
?customer-name ?delay-days
----------------------------
"Clara" 5
1 result(s) found.
[(- ?actual ?promise) ?delay-days] runs first and binds ?delay-days. Then [(> ?delay-days 0)] can use it because it now appears in a later clause. Reversing the two expression clauses would be a parse error — see the next section.
Minigraf validates that every variable referenced inside an expression is already bound at the point where the expression clause appears. This check runs during parsing, not at query time.
; PARSE ERROR — ?delay-days is referenced before it is bound
(query [:find ?customer-name
:where [?order :order/customer ?customer]
[?customer :customer/name ?customer-name]
[(> ?delay-days 0)] ; ← ?delay-days not yet bound
[?order :order/promise-epoch ?promise]
[?order :order/actual-epoch ?actual]
[(- ?actual ?promise) ?delay-days]])
Fix: move the binding clause before any clause that uses the variable it introduces.
; Correct ordering
(query [:find ?customer-name
:where [?order :order/customer ?customer]
[?customer :customer/name ?customer-name]
[?order :order/promise-epoch ?promise]
[?order :order/actual-epoch ?actual]
[(- ?actual ?promise) ?delay-days] ; binds ?delay-days first
[(> ?delay-days 0)]]) ; then filters on it
Four string predicates are available as filter clauses: starts-with?, ends-with?, contains?, and matches?. All four take a string variable as their first argument.
; Products whose SKU starts with "LP"
(query [:find ?name ?sku
:where [?p :product/name ?name]
[?p :product/sku ?sku]
[(starts-with? ?sku "LP")]])
?name ?sku
-----------------------
"LaptopPro 15" "LP-15"
1 result(s) found.
; Products whose name contains "Phone"
(query [:find ?name
:where [?p :product/name ?name]
[(contains? ?name "Phone")]])
?name
----------
"PhoneX 11"
"PhoneX 12"
2 result(s) found.
ends-with? works symmetrically to starts-with?. matches? accepts a regular expression string and returns true when the value matches anywhere in the string; an invalid regex is a parse error.
Type predicates filter bindings to rows where the value has a specific Minigraf type. They are useful when an attribute stores mixed-type values or when you want to assert that data is well-formed.
; Confirm all product prices are integers
(query [:find ?name ?price
:where [?p :product/name ?name]
[?p :product/price ?price]
[(integer? ?price)]])
?name ?price
----------------------------------------
"ClearView 27" Monitor" 449
"PhoneX 12" 799
"LaptopPro 15" 1229
"PhoneX 11" 599
"NoiseCancel Pro" 249
"USB-C Cable 2m" 19
"Compact Keyboard" 89
"BudgetBook 14" 699
8 result(s) found.
All eight products have integer prices, so all eight rows survive the filter.
; Confirm customer emails are strings
(query [:find ?name ?email
:where [?c :customer/name ?name]
[?c :customer/email ?email]
[(string? ?email)]])
?name ?email
--------------------------------------------
"Alice" "alice@example.com"
"Clara" "clara@example.com"
"Ben" "ben@example.com"
3 result(s) found.
Also available: float?, boolean?, nil?. Type predicates can be used as filters (as shown) or as bindings — [(integer? ?price) ?flag] binds true or false to ?flag, which is useful when you want the type check result as a column rather than a row selector.
Expression bindings compose with aggregates. The pattern is: bind the computed value to a variable inside :where, then aggregate that variable in :find.
; Total order value using qty × price, grouped by customer
(query [:find ?customer-name (sum ?line-total)
:where [?customer :customer/name ?customer-name]
[?order :order/customer ?customer]
[?item :order-item/order ?order]
[?item :order-item/price ?price]
[?item :order-item/qty ?qty]
[(* ?price ?qty) ?line-total]])
?customer-name (sum ?line-total)
----------------------------------
"Alice" 837
"Ben" 1299
2 result(s) found.
Alice's total is 837: $799 for the PhoneX 12 (alice-order-1-item-1) plus $19 + $19 for the two USB-C cables in alice-order-2. Ben's total is $1,299 for the laptop. Clara's items are excluded because none of her order items carry an :order-item/qty fact — the join silently produces no rows for her.
Note that this query omits :with. Without :with, ?customer-name is the sole grouping key and (sum) accumulates all matching line-totals into one row per customer. Adding :with ?item would make each item its own group, returning one row per item with each individual total rather than the per-customer grand total. See Section 6 for a detailed explanation of :with and grouping granularity.
-
Filter:
[(expr)]— keeps bindings where the expression is truthy; introduces no new variables. -
Binding:
[(expr) ?var]— evaluates the expression and binds the result to?var;?varis available to all later clauses. -
Variable binding order: every
?variablereferenced inside an expression must be bound by an earlier:whereclause — this is checked at parse time. -
Arithmetic operators:
+-*/— integer division truncates; divide-by-zero drops the row silently. -
Comparison operators:
<><=>==!=— work on integers, floats, and strings;=and!=also work on keywords. -
String predicates:
starts-with?ends-with?contains?matches?— take a string variable and a literal argument. -
Type predicates:
string?integer?float?boolean?nil?— usable as filters or as bindings that returntrue/false. -
Composing with aggregates: bind the computed value in
:where, then aggregate the variable in:find.
← Section 6: Aggregates | → Section 8: Prepared queries
Reference: Arithmetic & predicate expressions