substrait_explain/extensions/
examples.rs1use crate::extensions::args::{EnumValue, ExtensionArgs, ExtensionValue};
19use crate::extensions::registry::{Explainable, ExtensionError, ExtensionRegistry};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, prost::Enumeration)]
27#[repr(i32)]
28pub enum PartitionStrategy {
29 Unspecified = 0,
31 Hash = 1,
33 Range = 2,
35 Broadcast = 3,
37}
38
39impl PartitionStrategy {
40 pub fn as_str_name(self) -> &'static str {
42 match self {
43 PartitionStrategy::Unspecified => "UNSPECIFIED",
44 PartitionStrategy::Hash => "HASH",
45 PartitionStrategy::Range => "RANGE",
46 PartitionStrategy::Broadcast => "BROADCAST",
47 }
48 }
49
50 pub fn from_str_name(s: &str) -> Option<Self> {
52 match s {
53 "UNSPECIFIED" => Some(PartitionStrategy::Unspecified),
54 "HASH" => Some(PartitionStrategy::Hash),
55 "RANGE" => Some(PartitionStrategy::Range),
56 "BROADCAST" => Some(PartitionStrategy::Broadcast),
57 _ => None,
58 }
59 }
60}
61
62#[derive(Clone, PartialEq, prost::Message)]
94pub struct PartitionHint {
95 #[prost(enumeration = "PartitionStrategy", repeated, tag = "1")]
98 pub strategies: Vec<i32>,
99 #[prost(int64, tag = "2")]
101 pub count: i64,
102}
103
104impl prost::Name for PartitionHint {
105 const PACKAGE: &'static str = "example";
106 const NAME: &'static str = "PartitionHint";
107
108 fn full_name() -> String {
109 "example.PartitionHint".to_owned()
110 }
111
112 fn type_url() -> String {
113 "type.googleapis.com/example.PartitionHint".to_owned()
114 }
115}
116
117impl Explainable for PartitionHint {
118 fn name() -> &'static str {
119 "PartitionHint"
120 }
121
122 fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
123 let strategies: Result<Vec<i32>, ExtensionError> = args
125 .positional
126 .iter()
127 .map(|val| {
128 let EnumValue(ident) = EnumValue::try_from(val)?;
129 PartitionStrategy::from_str_name(&ident)
130 .map(|s| s as i32)
131 .ok_or_else(|| {
132 ExtensionError::InvalidArgument(format!(
133 "Unknown PartitionStrategy variant '&{ident}'; \
134 expected one of &UNSPECIFIED, &HASH, &RANGE, &BROADCAST"
135 ))
136 })
137 })
138 .collect();
139
140 let mut extractor = args.extractor();
141 let count: i64 = extractor.get_named_or("count", 0)?;
142 extractor.check_exhausted()?;
143
144 Ok(PartitionHint {
145 strategies: strategies?,
146 count,
147 })
148 }
149
150 fn to_args(&self) -> Result<ExtensionArgs, ExtensionError> {
151 let mut args = ExtensionArgs::default();
152 for &raw in &self.strategies {
153 let s = PartitionStrategy::try_from(raw).unwrap_or(PartitionStrategy::Unspecified);
154 args.positional
155 .push(ExtensionValue::Enum(s.as_str_name().to_owned()));
156 }
157 if self.count != 0 {
158 args.insert("count", self.count);
159 }
160 Ok(args)
161 }
162}
163
164#[derive(Clone, PartialEq, prost::Message)]
196pub struct PlanHint {
197 #[prost(string, tag = "1")]
199 pub hint: String,
200}
201
202impl prost::Name for PlanHint {
203 const PACKAGE: &'static str = "example";
204 const NAME: &'static str = "PlanHint";
205
206 fn full_name() -> String {
207 "example.PlanHint".to_owned()
208 }
209
210 fn type_url() -> String {
211 "type.googleapis.com/example.PlanHint".to_owned()
212 }
213}
214
215impl Explainable for PlanHint {
216 fn name() -> &'static str {
217 "PlanHint"
218 }
219
220 fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
221 if !args.positional.is_empty() {
222 return Err(ExtensionError::InvalidArgument(
223 "PlanHint does not accept positional arguments".to_owned(),
224 ));
225 }
226
227 let mut extractor = args.extractor();
228 let hint: String = extractor.expect_named_arg::<&str>("hint")?.to_owned();
229 extractor.check_exhausted()?;
230 Ok(PlanHint { hint })
231 }
232
233 fn to_args(&self) -> Result<ExtensionArgs, ExtensionError> {
234 let mut args = ExtensionArgs::default();
235 args.named
236 .insert("hint".to_owned(), ExtensionValue::String(self.hint.clone()));
237 Ok(args)
238 }
239}
240
241#[derive(Clone, PartialEq, prost::Message)]
274pub struct BlobStoreRead {
275 #[prost(string, tag = "1")]
277 pub path: String,
278 #[prost(int64, tag = "2")]
280 pub limit: i64,
281 #[prost(bool, tag = "3")]
283 pub include_archived: bool,
284}
285
286impl prost::Name for BlobStoreRead {
287 const PACKAGE: &'static str = "example";
288 const NAME: &'static str = "BlobStoreRead";
289
290 fn full_name() -> String {
291 "example.BlobStoreRead".to_owned()
292 }
293
294 fn type_url() -> String {
295 "type.googleapis.com/example.BlobStoreRead".to_owned()
296 }
297}
298
299impl Explainable for BlobStoreRead {
300 fn name() -> &'static str {
301 "BlobStoreRead"
302 }
303
304 fn from_args(args: &ExtensionArgs) -> Result<Self, ExtensionError> {
305 if args.positional.len() != 1 {
306 return Err(ExtensionError::InvalidArgument(format!(
307 "BlobStoreRead expects exactly 1 positional path argument, got {}",
308 args.positional.len()
309 )));
310 }
311 if !args.output_columns.is_empty() {
312 return Err(ExtensionError::InvalidArgument(
313 "BlobStoreRead output columns belong in Read:Extension[...]".to_owned(),
314 ));
315 }
316
317 let path = <&str>::try_from(&args.positional[0])?.to_owned();
318 let mut extractor = args.extractor();
319 let limit: i64 = extractor.get_named_or("limit", 0)?;
320 let include_archived: bool = extractor.get_named_or("include_archived", false)?;
321 extractor.check_exhausted()?;
322
323 Ok(Self {
324 path,
325 limit,
326 include_archived,
327 })
328 }
329
330 fn to_args(&self) -> Result<ExtensionArgs, ExtensionError> {
331 let mut args = ExtensionArgs::default();
332 args.positional
333 .push(ExtensionValue::String(self.path.clone()));
334 if self.limit != 0 {
335 args.named
336 .insert("limit".to_owned(), ExtensionValue::Integer(self.limit));
337 }
338 if self.include_archived {
339 args.named
340 .insert("include_archived".to_owned(), ExtensionValue::Boolean(true));
341 }
342 Ok(args)
343 }
344}
345
346pub fn registry() -> ExtensionRegistry {
348 let mut registry = ExtensionRegistry::new();
349 registry
350 .register_enhancement::<PartitionHint>()
351 .expect("register PartitionHint example enhancement");
352 registry
353 .register_optimization::<PlanHint>()
354 .expect("register PlanHint example optimization");
355 registry
356 .register_extension_table::<BlobStoreRead>()
357 .expect("register BlobStoreRead example extension table");
358 registry
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::extensions::AnyConvertible;
365
366 fn make_hint(strategies: Vec<PartitionStrategy>, count: i64) -> PartitionHint {
367 PartitionHint {
368 strategies: strategies.into_iter().map(|s| s as i32).collect(),
369 count,
370 }
371 }
372
373 #[test]
374 fn round_trip_via_any() {
375 let original = make_hint(vec![PartitionStrategy::Hash, PartitionStrategy::Range], 4);
376 let any = original.to_any().expect("encode");
377 let decoded = PartitionHint::from_any(any.as_ref()).expect("decode");
378 assert_eq!(original, decoded);
379 }
380
381 #[test]
382 fn to_args_produces_enum_and_named() {
383 let hint = make_hint(vec![PartitionStrategy::Hash], 8);
384 let args = hint.to_args().unwrap();
385 assert_eq!(args.positional.len(), 1);
386 assert!(matches!(&args.positional[0], ExtensionValue::Enum(s) if s == "HASH"));
387 let count = args.named.get("count").expect("count arg");
388 assert_eq!(i64::try_from(count).unwrap(), 8);
389 }
390
391 #[test]
392 fn to_args_omits_zero_count() {
393 let hint = make_hint(vec![PartitionStrategy::Broadcast], 0);
394 let args = hint.to_args().unwrap();
395 assert!(args.named.is_empty(), "count=0 should be omitted");
396 }
397
398 #[test]
399 fn from_args_round_trip() {
400 let original = make_hint(vec![PartitionStrategy::Hash, PartitionStrategy::Range], 16);
401 let args = original.to_args().unwrap();
402 let decoded = PartitionHint::from_args(&args).unwrap();
403 assert_eq!(original, decoded);
404 }
405
406 #[test]
407 fn from_args_rejects_unknown_strategy() {
408 let mut args = ExtensionArgs::default();
409 args.positional
410 .push(ExtensionValue::Enum("BOGUS".to_owned()));
411 assert!(PartitionHint::from_args(&args).is_err());
412 }
413
414 #[test]
415 fn from_args_rejects_non_enum_positional() {
416 let mut args = ExtensionArgs::default();
418 args.push(1_i64);
419 let result = PartitionHint::from_args(&args);
420 assert!(
421 result.is_err(),
422 "expected error for non-enum positional arg, got {result:?}"
423 );
424 }
425
426 #[test]
427 fn from_args_rejects_extra_named_args() {
428 let mut args = ExtensionArgs::default();
430 args.insert("unknown_key", 99_i64);
431 let result = PartitionHint::from_args(&args);
432 assert!(
433 result.is_err(),
434 "expected error for unknown named arg, got {result:?}"
435 );
436 }
437
438 #[test]
439 fn from_args_empty_strategies_roundtrip() {
440 let original = make_hint(vec![], 0);
441 let args = original.to_args().unwrap();
442 let decoded = PartitionHint::from_args(&args).unwrap();
443 assert_eq!(original, decoded);
444 assert!(decoded.strategies.is_empty());
445 assert_eq!(decoded.count, 0);
446 }
447
448 #[test]
449 fn registry_roundtrip() {
450 let registry = registry();
451
452 let original = make_hint(vec![PartitionStrategy::Hash], 4);
453 let any = original.to_any().unwrap();
454
455 let (name, args) = registry
456 .decode_enhancement(any.as_ref())
457 .expect("decode_enhancement");
458 assert_eq!(name, "PartitionHint");
459 assert_eq!(args.positional.len(), 1);
460
461 let any2 = registry
462 .parse_enhancement("PartitionHint", &args)
463 .expect("parse_enhancement");
464 let decoded = PartitionHint::from_any(any2.as_ref()).unwrap();
465 assert_eq!(original, decoded);
466 }
467
468 #[test]
469 fn plan_hint_args_round_trip() {
470 let original = PlanHint {
471 hint: "use_index".to_owned(),
472 };
473 let args = original.to_args().unwrap();
474 let decoded = PlanHint::from_args(&args).unwrap();
475 assert_eq!(original, decoded);
476 }
477
478 #[test]
479 fn plan_hint_registry_roundtrip() {
480 let registry = registry();
481
482 let original = PlanHint {
483 hint: "parallel".to_owned(),
484 };
485 let any = original.to_any().unwrap();
486
487 let (name, args) = registry
488 .decode_optimization(any.as_ref())
489 .expect("decode_optimization");
490 assert_eq!(name, "PlanHint");
491
492 let any2 = registry
493 .parse_optimization("PlanHint", &args)
494 .expect("parse_optimization");
495 let decoded = PlanHint::from_any(any2.as_ref()).unwrap();
496 assert_eq!(original, decoded);
497 }
498
499 #[test]
500 fn blob_store_read_args_round_trip() {
501 let original = BlobStoreRead {
502 path: "path/to/file".to_owned(),
503 limit: 100,
504 include_archived: true,
505 };
506
507 let args = original.to_args().unwrap();
508 let decoded = BlobStoreRead::from_args(&args).unwrap();
509
510 assert_eq!(original, decoded);
511 }
512
513 #[test]
514 fn blob_store_read_registry_roundtrip() {
515 let registry = registry();
516
517 let original = BlobStoreRead {
518 path: "path/to/file".to_owned(),
519 limit: 100,
520 include_archived: true,
521 };
522 let any = original.to_any().unwrap();
523
524 let (name, args) = registry
525 .decode_extension_table(any.as_ref())
526 .expect("decode_extension_table");
527 assert_eq!(name, "BlobStoreRead");
528
529 let any2 = registry
530 .parse_extension_table("BlobStoreRead", &args)
531 .expect("parse_extension_table");
532 let decoded = BlobStoreRead::from_any(any2.as_ref()).unwrap();
533 assert_eq!(original, decoded);
534 }
535}