mod common;
use common::{assert_roundtrip_canonical, roundtrip_plan};
use substrait_explain::{ParseError, Parser};
#[test]
fn test_simple_plan_roundtrip() {
let plan = r#"=== Plan
Root[c, d]
Filter[$0 => $0, $1]
Project[$1, 42]
Read[my.table => a:i32, b:string?, c:boolean]"#;
roundtrip_plan(plan);
}
#[test]
fn test_plan_with_extensions_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: add
# 11 @ 1: multiply
=== Plan
Root[result]
Project[$0, $1, add($0, $1):i32?]
Read[table1 => col1:i32?, col2:i32?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_complex_plan_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
@ 2: https://github.com/substrait-io/substrait/blob/main/extensions/functions_aggregate.yaml
Functions:
# 10 @ 1: add
# 11 @ 2: sum
# 12 @ 2: count
=== Plan
Root[name, parent, sum, count]
Project[$0, $3, $1, $2]
Read[schema.table => name:string?, parent:string?, sum:fp64?, count:fp64?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_plan_with_verbose_and_simple_output() {
let simple_plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: coalesce
=== Plan
Root[name, num]
Project[$1, coalesce($1, $2):fp64?]
Read[schema.table => name:string?, num:fp64?, other_num:fp64?, id:i64]"#;
let verbose_plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: coalesce
=== Plan
Root[name, num]
Project[$1, coalesce#10($1, $2):fp64?]
Read[schema.table => name:string?, num:fp64?, other_num:fp64?, id:i64]"#;
assert_roundtrip_canonical(simple_plan, verbose_plan);
}
#[test]
fn test_multiple_relations_roundtrip() {
let plan = r#"=== Plan
Root[name, num, id]
Project[$0, $1, $2]
Read[t1 => name:string?, num:fp64, id:i64]
Root[name, num, id]
Filter[$0 => $0, $1, $2]
Read[schema.table => name:string?, num:fp64?, id:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_plan_with_fetch_and_sort_roundtrip() {
let plan = r#"=== Plan
Root[name, num, id]
Project[$0, $1, $2]
Read[schema.table => name:string?, num:fp64?, id:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_plan_with_extension_leaf_roundtrip() {
let plan = r#"=== Plan
Root[name, some_value]
Project[$0, $1]
Read[schema.table => name:string?, some_value:fp64?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_aggregate_relation_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_aggregate.yaml
Functions:
# 10 @ 1: sum
# 11 @ 1: count
=== Plan
Root[category, total, count]
Aggregate[$0 => sum($1):fp64?, count($1):i64]
Read[orders => category:string?, amount:fp64?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_aggregate_multiple_grouping_sets_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_aggregate.yaml
Functions:
# 10 @ 1: sum
# 11 @ 1: count
=== Plan
Root[sum, category, count]
Aggregate[($0, $1), ($0), _ => sum($2):fp64?, $0, count($2):i64]
Read[orders => category:string?, region:string?, amount:fp64?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_global_aggregate_relation_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_aggregate.yaml
Functions:
# 10 @ 1: sum
# 11 @ 1: count
=== Plan
Root[sum, count]
Aggregate[_ => sum($2):fp64?, count($2):i64]
Read[orders => category:string?, region:string?, amount:fp64?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_sort_relation_roundtrip() {
let plan = r#"=== Plan
Root[a, b]
Sort[($0, &AscNullsFirst), ($1, &DescNullsLast) => $0, $1]
Read[table => a:i32, b:string]"#;
roundtrip_plan(plan);
}
#[test]
fn test_fetch_relation_roundtrip() {
let plan_both = r#"=== Plan
Root[a, b]
Fetch[limit=10, offset=5 => $0, $1]
Read[table => a:i32, b:string]"#;
let plan_offset_first = r#"=== Plan
Root[a, b]
Fetch[offset=5, limit=10 => $0, $1]
Read[table => a:i32, b:string]"#;
assert_roundtrip_canonical(plan_both, plan_offset_first);
let plan_limit_only = r#"=== Plan
Root[a, b]
Fetch[limit=10 => $0, $1]
Read[table => a:i32, b:string]"#;
roundtrip_plan(plan_limit_only);
let plan_offset_only = r#"=== Plan
Root[a, b]
Fetch[offset=5 => $0, $1]
Read[table => a:i32, b:string]"#;
roundtrip_plan(plan_offset_only);
let plan_empty = r#"=== Plan
Root[a, b]
Fetch[_ => $0, $1]
Read[table => a:i32, b:string]"#;
roundtrip_plan(plan_empty);
}
#[test]
fn test_full_name_zero_arg_type_signature_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_aggregate_generic
Functions:
# 1 @ 1: count:
=== Plan
Root[n]
Project[count():i64]
Read[events => n:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_no_sig_function_unique_no_anchor() {
let plan = r#"=== Extensions
URNs:
@ 1: urn:substrait:vendor_a
Functions:
# 1 @ 1: add
=== Plan
Root[result]
Project[$0, $1, add($0, $1):i64]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_ambiguous_base_name_fails() {
let plan = r#"=== Extensions
URNs:
@ 1: urn:substrait:vendor_a
Functions:
# 1 @ 1: add
# 2 @ 1: add:
=== Plan
Root[result]
Project[add($0, $1)]
Read[t => a:i64, b:i64]"#;
assert!(Parser::parse(plan).is_err());
}
#[test]
fn test_no_sig_and_zero_arg_disambiguate_without_anchor() {
let plan = r#"=== Extensions
URNs:
@ 1: urn:substrait:vendor_a
Functions:
# 1 @ 1: add
# 2 @ 1: add:
=== Plan
Root[result]
Project[$0, $1, add#1($0, $1):i64, add:($0, $1):i64]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_three_add_variants_all_require_anchors() {
let plan = r#"=== Extensions
URNs:
@ 1: urn:substrait:vendor_a
@ 2: urn:substrait:vendor_b
Functions:
# 1 @ 1: add
# 2 @ 1: add:
# 3 @ 2: add:
=== Plan
Root[result]
Project[$0, $1, add#1($0, $1):i64, add:#2($0, $1):i64, add:#3($0, $1):i64]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_u_prefix_type_in_schema_roundtrip() {
let canonical = r#"=== Extensions
URNs:
@ 1: https://example.com/types
Types:
# 5 @ 1: json
=== Plan
Root[result]
Project[$0]
Read[data => doc:json]"#;
let with_prefix = r#"=== Extensions
URNs:
@ 1: https://example.com/types
Types:
# 5 @ 1: json
=== Plan
Root[result]
Project[$0]
Read[data => doc:u!json]"#;
assert_roundtrip_canonical(canonical, with_prefix);
}
#[test]
fn test_u_prefix_type_in_function_declaration_roundtrip() {
let canonical = r#"=== Extensions
URNs:
@ 1: https://example.com/funcs
Functions:
# 5 @ 1: json_extract_path:u!json_str
=== Plan
Root[result]
Project[json_extract_path($0, $1):string]
Read[data => doc:string, path:string]"#;
let with_signature = r#"=== Extensions
URNs:
@ 1: https://example.com/funcs
Functions:
# 5 @ 1: json_extract_path:u!json_str
=== Plan
Root[result]
Project[json_extract_path:u!json_str($0, $1):string]
Read[data => doc:string, path:string]"#;
roundtrip_plan(canonical);
assert_roundtrip_canonical(canonical, with_signature);
}
#[test]
fn test_u_prefix_type_in_cast_roundtrip() {
let canonical = r#"=== Extensions
URNs:
@ 1: https://example.com/types
Types:
# 5 @ 1: json
=== Plan
Root[result]
Project[($0)::json]
Read[data => doc:string]"#;
let with_prefix = r#"=== Extensions
URNs:
@ 1: https://example.com/types
Types:
# 5 @ 1: json
=== Plan
Root[result]
Project[($0)::u!json]
Read[data => doc:string]"#;
roundtrip_plan(canonical);
assert_roundtrip_canonical(canonical, with_prefix);
}
#[test]
fn test_join_relation_roundtrip() {
let plan = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: eq
=== Plan
Root[id, name, amount]
Join[&Inner, eq($0, $2):boolean => $0, $1, $3]
Read[users => id:i64, name:string]
Read[orders => user_id:i64, amount:i32]"#;
roundtrip_plan(plan);
}
#[test]
fn test_join_relation_semi_types_roundtrip() {
let plan_semi = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: eq
=== Plan
Root[a, b]
Join[&LeftSemi, eq($0, $2):boolean => $0, $1]
Read[left_table => a:i32, b:string]
Read[right_table => c:i32, d:string]"#;
roundtrip_plan(plan_semi);
}
#[test]
fn test_join_relation_right_mark_roundtrip() {
let plan_right_mark = r#"=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: eq
=== Plan
Root[d, mark]
Join[&RightMark, eq($0, $2):boolean => $2, $3]
Read[left_table => a:i32, b:string]
Read[right_table => c:i32, d:string, e:boolean]"#;
roundtrip_plan(plan_right_mark);
}
#[test]
fn test_unregistered_extension_error() {
let plan_text = r#"=== Plan
Root[result]
ExtensionLeaf:UnknownExtension[path='test.parquet' => col1:i32]"#;
let err = Parser::parse(plan_text).expect_err("parse should fail for unknown extension");
match err {
ParseError::UnregisteredExtension { name, context } => {
assert_eq!(name, "UnknownExtension");
assert_eq!(context.line_no, 3);
assert!(
context.line.contains("ExtensionLeaf:UnknownExtension"),
"context should include the extension line"
);
}
other => panic!("expected unregistered extension error, got {other:?}"),
}
}
#[test]
fn test_malformed_input_error_handling() {
use substrait_explain::Parser;
let malformed_plan = r#"=== Plan
Root[result]
ExtensionLeaf:TestExtension[path='test.parquet', missing_close => col1:i32"#;
let result = Parser::parse(malformed_plan);
assert!(
result.is_err(),
"Malformed input should result in parse error"
);
let error = result.unwrap_err();
let error_msg = format!("{error}");
assert!(error_msg.len() > 10, "Error message should be descriptive");
assert!(
error_msg.contains("3") || error_msg.contains("line"),
"Error should reference line information: {error_msg}"
);
}
#[test]
fn test_overloaded_functions_verbose_shows_signature_and_anchor() {
let simple = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_comparison
Functions:
# 1 @ 1: equal:any_any
# 2 @ 1: equal:str_str
=== Plan
Root[result]
Project[$0, equal:any_any($0, $1):boolean, equal:str_str($0, $1):boolean]
Read[t => a:i64, b:i64, c:string]"#;
let verbose = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_comparison
Functions:
# 1 @ 1: equal:any_any
# 2 @ 1: equal:str_str
=== Plan
Root[result]
Project[$0, equal:any_any#1($0, $1):boolean, equal:str_str#2($0, $1):boolean]
Read[t => a:i64, b:i64, c:string]"#;
roundtrip_plan(simple);
assert_roundtrip_canonical(simple, verbose);
}
#[test]
fn test_unique_function_compact_omits_signature() {
let compact = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_arithmetic
Functions:
# 1 @ 1: add:i64_i64
=== Plan
Root[result]
Project[$0, add($0, $1):i64]
Read[t => a:i64, b:i64]"#;
let compound = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_arithmetic
Functions:
# 1 @ 1: add:i64_i64
=== Plan
Root[result]
Project[$0, add:i64_i64($0, $1):i64]
Read[t => a:i64, b:i64]"#;
let verbose = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_arithmetic
Functions:
# 1 @ 1: add:i64_i64
=== Plan
Root[result]
Project[$0, add:i64_i64#1($0, $1):i64]
Read[t => a:i64, b:i64]"#;
assert_roundtrip_canonical(compact, compound);
assert_roundtrip_canonical(compact, verbose);
}
#[test]
fn test_mixed_unique_and_overloaded_functions() {
let simple = r#"=== Extensions
URNs:
@ 1: extension:io.substrait:functions_comparison
@ 2: extension:io.substrait:functions_string
Functions:
# 1 @ 1: equal:any_any
# 2 @ 1: equal:str_str
# 3 @ 2: like:str_str
=== Plan
Root[result]
Project[$0, equal:any_any($0, $1):boolean, equal:str_str($0, $2):boolean, like($0, $2):boolean]
Read[t => id:i64, score:i64, name:string]"#;
roundtrip_plan(simple);
}
#[test]
fn test_same_compound_name_two_urns_requires_anchor() {
let plan = r#"=== Extensions
URNs:
@ 1: urn:substrait:vendor_a
@ 2: urn:substrait:vendor_b
Functions:
# 1 @ 1: equal:any_any
# 2 @ 2: equal:any_any
=== Plan
Root[result]
Project[$0, equal:any_any#1($0, $1):boolean, equal:any_any#2($0, $1):boolean]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_inline_single_row() {
let plan = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, 'alice') => id:i64, name:string]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_inline_multi_row() {
let plan = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, 'alice'), (2, 'bob') => id:i64, name:string]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_typed_null_roundtrip() {
let plan = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, null:string?), (2, 'bob') => id:i64, name:string?]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_empty() {
let plan = r#"
=== Plan
Root[id, name]
Read:Virtual[_ => id:i64, name:string]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_single_column() {
let plan = r#"
=== Plan
Root[id]
Read:Virtual[
- (1),
- (2),
- (3)
- => id:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_in_plan_context() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: gt
=== Plan
Root[name]
Filter[gt($0, 1):boolean => $0, $1]
Read:Virtual[
- (1, 'alice'),
- (2, 'bob'),
- (3, 'carol')
- => id:i64, name:string]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_no_columns() {
let plan = r#"
=== Plan
Root[]
Read:Virtual[(), () => ]"#;
roundtrip_plan(plan);
}
#[test]
fn test_virtual_read_invalid_expression_returns_error() {
let plan = r#"
=== Plan
Root[result]
Read:Virtual[(unknown_func(1)) => result:i64]"#;
let result = Parser::parse(plan);
assert!(
result.is_err(),
"undeclared function in VirtualTable row should fail"
);
}
#[test]
fn test_emit_read_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: add
=== Plan
Root[sum]
Project[add($0, $1):i64]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_filter_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
@ 2: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: add
# 11 @ 2: gt
=== Plan
Root[sum]
Project[add($0, $1):i64]
Filter[gt($2, 0):boolean => $0, $1]
Read[t => a:i64, b:i64, c:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_sort_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: add
=== Plan
Root[sum]
Project[add($0, $1):i64]
Sort[($0, &AscNullsFirst) => $0, $1]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_fetch_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: add
=== Plan
Root[sum]
Project[add($0, $1):i64]
Fetch[limit=10, offset=0 => $0, $1]
Read[t => a:i64, b:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_join_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
@ 2: https://github.com/substrait-io/substrait/blob/main/extensions/functions_comparison.yaml
Functions:
# 10 @ 1: add
# 11 @ 2: eq
=== Plan
Root[sum]
Project[add($1, $3):i64]
Join[&Inner, eq($0, $2):boolean => $0, $1, $2, $3]
Read[users => id:i64, age:i64]
Read[orders => user_id:i64, amount:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_aggregate_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
@ 2: https://github.com/substrait-io/substrait/blob/main/extensions/functions_aggregate.yaml
Functions:
# 10 @ 1: add
# 11 @ 2: sum
=== Plan
Root[total]
Project[add($0, $1):i64]
Aggregate[$0 => $0, sum($1):i64]
Read[t => category:i64, amount:i64]"#;
roundtrip_plan(plan);
}
#[test]
fn test_emit_virtual_read_to_project_with_expression() {
let plan = r#"
=== Extensions
URNs:
@ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
Functions:
# 10 @ 1: add
=== Plan
Root[sum]
Project[add($0, $1):i64]
Read:Virtual[(1, 2), (3, 4) => a:i64, b:i64]"#;
roundtrip_plan(plan);
}