|
| 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