Skip to content

Commit 2f156f0

Browse files
authored
Merge pull request #237 from project-minigraf/feature/tutorial-series-234
docs: Datalog tutorial series — 11 sections, Corestore storyline (closes #234)
2 parents 1e0be61 + 9d444a1 commit 2f156f0

2 files changed

Lines changed: 309 additions & 0 deletions

File tree

demos/tutorial_corestore_setup.txt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# ================================================================
2+
# Corestore Tutorial Dataset
3+
# ================================================================
4+
# Base setup for the Minigraf Datalog tutorial series.
5+
# All tutorial sections assume this has been loaded first.
6+
#
7+
# Run with: cargo run < demos/tutorial_corestore_setup.txt
8+
#
9+
# After loading: tx_count = 3
10+
# ================================================================
11+
12+
# ── tx 1: Category hierarchy ─────────────────────────────────────
13+
# Electronics
14+
# ├── Laptops
15+
# ├── Mobile
16+
# ├── Audio
17+
# │ └── Headphones
18+
# │ └── Noise-Cancelling
19+
# └── Accessories
20+
21+
(transact [
22+
[:cat-electronics :category/name "Electronics"]
23+
[:cat-laptops :category/name "Laptops"]
24+
[:cat-laptops :category/parent :cat-electronics]
25+
[:cat-mobile :category/name "Mobile"]
26+
[:cat-mobile :category/parent :cat-electronics]
27+
[:cat-audio :category/name "Audio"]
28+
[:cat-audio :category/parent :cat-electronics]
29+
[:cat-headphones :category/name "Headphones"]
30+
[:cat-headphones :category/parent :cat-audio]
31+
[:cat-nc :category/name "Noise-Cancelling"]
32+
[:cat-nc :category/parent :cat-headphones]
33+
[:cat-accessories :category/name "Accessories"]
34+
[:cat-accessories :category/parent :cat-electronics]
35+
])
36+
37+
# ── tx 2: Product catalog ─────────────────────────────────────────
38+
# Two products per major leaf category for window function examples.
39+
40+
(transact [
41+
[:laptop-pro :product/name "LaptopPro 15"]
42+
[:laptop-pro :product/sku "LP-15"]
43+
[:laptop-pro :product/price 1299]
44+
[:laptop-pro :product/category :cat-laptops]
45+
46+
[:laptop-budget :product/name "BudgetBook 14"]
47+
[:laptop-budget :product/sku "LB-14"]
48+
[:laptop-budget :product/price 699]
49+
[:laptop-budget :product/category :cat-laptops]
50+
51+
[:phone-x :product/name "PhoneX 12"]
52+
[:phone-x :product/sku "PX-12"]
53+
[:phone-x :product/price 799]
54+
[:phone-x :product/category :cat-mobile]
55+
56+
[:phone-prev :product/name "PhoneX 11"]
57+
[:phone-prev :product/sku "PX-11"]
58+
[:phone-prev :product/price 599]
59+
[:phone-prev :product/category :cat-mobile]
60+
61+
[:nc-headphones :product/name "NoiseCancel Pro"]
62+
[:nc-headphones :product/sku "NC-PRO"]
63+
[:nc-headphones :product/price 249]
64+
[:nc-headphones :product/category :cat-nc]
65+
66+
[:usb-cable :product/name "USB-C Cable 2m"]
67+
[:usb-cable :product/sku "USB-C-2M"]
68+
[:usb-cable :product/price 19]
69+
[:usb-cable :product/category :cat-accessories]
70+
71+
[:keyboard-k1 :product/name "Compact Keyboard"]
72+
[:keyboard-k1 :product/sku "KB-K1"]
73+
[:keyboard-k1 :product/price 89]
74+
[:keyboard-k1 :product/category :cat-accessories]
75+
76+
[:monitor-27 :product/name "ClearView 27\" Monitor"]
77+
[:monitor-27 :product/sku "CV-27"]
78+
[:monitor-27 :product/price 449]
79+
[:monitor-27 :product/category :cat-electronics]
80+
])
81+
82+
# ── tx 3: Customers ───────────────────────────────────────────────
83+
84+
(transact [
85+
[:alice :customer/name "Alice"]
86+
[:alice :customer/email "alice@example.com"]
87+
[:ben :customer/name "Ben"]
88+
[:ben :customer/email "ben@example.com"]
89+
[:clara :customer/name "Clara"]
90+
[:clara :customer/email "clara@example.com"]
91+
])

examples/tutorial_udfs.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//! Tutorial Section 10 — User-Defined Functions
2+
//!
3+
//! Demonstrates `register_predicate` and `register_aggregate` in the context
4+
//! of the Corestore e-commerce scenario.
5+
//!
6+
//! Run with:
7+
//! cargo run --example tutorial_udfs
8+
9+
use minigraf::{Minigraf, QueryResult, Value};
10+
11+
fn main() -> anyhow::Result<()> {
12+
let db = Minigraf::in_memory()?;
13+
14+
// ── Seed data ────────────────────────────────────────────────────────────
15+
//
16+
// Promo codes: one valid Corestore code, one invalid short code, one from
17+
// a different scheme.
18+
19+
db.execute(
20+
r#"(transact [
21+
[:promo-1 :promo/code "CORESTORE-SUMMER2026"]
22+
[:promo-2 :promo/code "SAVE10"]
23+
[:promo-3 :promo/code "PARTNER-EXCLUSIVE"]
24+
])"#,
25+
)?;
26+
27+
// Customers and their orders with on-time-flag attributes.
28+
// Alice: order-a (on time), order-b (late) → score = 1/2 = 0.5
29+
// Ben: order-c (on time) → score = 1/1 = 1.0
30+
31+
db.execute(
32+
r#"(transact [
33+
[:alice :customer/name "Alice"]
34+
[:ben :customer/name "Ben"]
35+
[:order-a :order/customer :alice]
36+
[:order-a :order/on-time-flag 1]
37+
[:order-b :order/customer :alice]
38+
[:order-b :order/on-time-flag 0]
39+
[:order-c :order/customer :ben]
40+
[:order-c :order/on-time-flag 1]
41+
])"#,
42+
)?;
43+
44+
// ── Predicate UDF: valid-promo? ───────────────────────────────────────────
45+
//
46+
// A valid Corestore promo code must start with "CORESTORE-" and be at
47+
// least 15 characters long.
48+
49+
db.register_predicate("valid-promo?", |v: &Value| {
50+
if let Value::String(s) = v {
51+
s.starts_with("CORESTORE-") && s.len() >= 15
52+
} else {
53+
false
54+
}
55+
})?;
56+
57+
println!("=== Predicate UDF: valid-promo? ===");
58+
println!();
59+
println!("Query: find promo codes that satisfy valid-promo?");
60+
println!();
61+
println!(" (query [:find ?code");
62+
println!(" :where [?p :promo/code ?code]");
63+
println!(" [(valid-promo? ?code)]])");
64+
println!();
65+
66+
let promo_result = db.execute(
67+
r#"(query [:find ?code
68+
:where [?p :promo/code ?code]
69+
[(valid-promo? ?code)]])"#,
70+
)?;
71+
72+
let (promo_rows, promo_count) = match promo_result {
73+
QueryResult::QueryResults { results, .. } => {
74+
let n = results.len();
75+
(results, n)
76+
}
77+
_ => (vec![], 0),
78+
};
79+
80+
println!("?code");
81+
println!("--------------------");
82+
for row in &promo_rows {
83+
for val in row {
84+
match val {
85+
Value::String(s) => print!("\"{}\"", s),
86+
other => print!("{:?}", other),
87+
}
88+
}
89+
println!();
90+
}
91+
println!();
92+
println!("{} result(s) found.", promo_count);
93+
println!();
94+
95+
// ── Aggregate UDF: delivery-score ────────────────────────────────────────
96+
//
97+
// delivery-score receives a stream of integer flags (1 = on time, 0 = late)
98+
// and returns on_time_count / total_count as a float.
99+
//
100+
// Accumulator state: (on_time_count: i64, total_count: i64)
101+
102+
db.register_aggregate(
103+
"delivery-score",
104+
|| (0i64, 0i64),
105+
|state: &mut (i64, i64), val: &Value| {
106+
if let Value::Integer(flag) = val {
107+
state.1 += 1;
108+
if *flag == 1 {
109+
state.0 += 1;
110+
}
111+
}
112+
},
113+
|state: &(i64, i64), _n: usize| {
114+
if state.1 == 0 {
115+
Value::Null
116+
} else {
117+
Value::Float(state.0 as f64 / state.1 as f64)
118+
}
119+
},
120+
)?;
121+
122+
println!("=== Aggregate UDF: delivery-score ===");
123+
println!();
124+
println!("Query: on-time delivery score per customer");
125+
println!();
126+
println!(" (query [:find ?name (delivery-score ?flag)");
127+
println!(" :where [?customer :customer/name ?name]");
128+
println!(" [?order :order/customer ?customer]");
129+
println!(" [?order :order/on-time-flag ?flag]])");
130+
println!();
131+
132+
let score_result = db.execute(
133+
r#"(query [:find ?name (delivery-score ?flag)
134+
:where [?customer :customer/name ?name]
135+
[?order :order/customer ?customer]
136+
[?order :order/on-time-flag ?flag]])"#,
137+
)?;
138+
139+
let (score_rows, score_count) = match score_result {
140+
QueryResult::QueryResults { results, .. } => {
141+
let n = results.len();
142+
(results, n)
143+
}
144+
_ => (vec![], 0),
145+
};
146+
147+
println!("?name (delivery-score ?flag)");
148+
println!("------------------------------------");
149+
for row in &score_rows {
150+
let name = match &row[0] {
151+
Value::String(s) => format!("\"{}\"", s),
152+
other => format!("{:?}", other),
153+
};
154+
let score = match &row[1] {
155+
Value::Float(f) => {
156+
// Always show at least one decimal place for clarity
157+
if f.fract() == 0.0 {
158+
format!("{:.1}", f)
159+
} else {
160+
format!("{}", f)
161+
}
162+
}
163+
Value::Null => "null".to_string(),
164+
other => format!("{:?}", other),
165+
};
166+
println!("{:<10}{}", name, score);
167+
}
168+
println!();
169+
println!("{} result(s) found.", score_count);
170+
171+
// ── UDF aggregate in a window clause ─────────────────────────────────────────
172+
println!();
173+
println!("=== UDF aggregate in a window clause ===");
174+
println!();
175+
println!("Query: annotate each order with its customer delivery score");
176+
println!();
177+
178+
let window_result = db.execute(
179+
r#"(query [:find ?order (delivery-score ?flag :over (:partition-by ?customer :order-by ?order))
180+
:where [?customer :customer/name ?name]
181+
[?order :order/customer ?customer]
182+
[?order :order/on-time-flag ?flag]])"#,
183+
)?;
184+
185+
let (window_rows, window_count) = match window_result {
186+
QueryResult::QueryResults { results, .. } => {
187+
let n = results.len();
188+
(results, n)
189+
}
190+
_ => (vec![], 0),
191+
};
192+
193+
println!("?order (delivery-score ?flag :over ...)");
194+
println!("--------------------------------------------");
195+
for row in &window_rows {
196+
let order = match &row[0] {
197+
Value::Keyword(k) => format!(":{}", k),
198+
Value::Ref(u) => format!("{}", &u.to_string()[..8]),
199+
other => format!("{:?}", other),
200+
};
201+
let score = match &row[1] {
202+
Value::Float(f) => {
203+
if f.fract() == 0.0 {
204+
format!("{:.1}", f)
205+
} else {
206+
format!("{}", f)
207+
}
208+
}
209+
Value::Null => "null".to_string(),
210+
other => format!("{:?}", other),
211+
};
212+
println!("{:<12}{}", order, score);
213+
}
214+
println!();
215+
println!("{} result(s) found.", window_count);
216+
217+
Ok(())
218+
}

0 commit comments

Comments
 (0)