mod common;
use common::assert_roundtrip_canonical;
use substrait_explain::{OutputOptions, Parser, format_with_options};
#[test]
fn test_virtual_read_multiline_single_row() {
let inline = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, 'alice') => id:i64, name:string]"#;
let multiline = r#"
=== Plan
Root[id, name]
Read:Virtual[
- (1, 'alice')
- => id:i64, name:string]"#;
assert_roundtrip_canonical(inline.trim(), multiline.trim());
}
#[test]
fn test_virtual_read_multiline_multi_row() {
let multiline = r#"
=== Plan
Root[id, name]
Read:Virtual[
- (1, 'alice'),
- (2, 'bob'),
- (3, 'carol')
- => id:i64, name:string]"#;
let inline = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, 'alice'), (2, 'bob'), (3, 'carol') => id:i64, name:string]"#;
assert_roundtrip_canonical(multiline.trim(), inline.trim());
}
#[test]
fn test_virtual_read_multiline_typed_null() {
let inline = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, null:string?), (2, 'bob') => id:i64, name:string?]"#;
let multiline = r#"
=== Plan
Root[id, name]
Read:Virtual[
- (1, null:string?),
- (2, 'bob')
- => id:i64, name:string?]"#;
assert_roundtrip_canonical(inline.trim(), multiline.trim());
}
#[test]
fn test_virtual_read_multiline_nested_in_plan() {
let inline = 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') => id:i64, name:string]"#;
let multiline = 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')
- => id:i64, name:string]"#;
assert_roundtrip_canonical(inline.trim(), multiline.trim());
}
#[test]
fn test_virtual_read_multiline_tolerates_trailing_whitespace() {
let inline = "=== Plan\n\
Root[id, name]\n \
Read:Virtual[(1, 'alice'), (2, 'bob') => id:i64, name:string]";
let multiline = concat!(
"=== Plan\n",
"Root[id, name]\n",
" Read:Virtual[ \n", " - (1, 'alice'), \n", " - (2, 'bob') \n", " - => id:i64, name:string]",
);
assert_roundtrip_canonical(inline, multiline);
}
#[test]
fn test_virtual_read_multiline_rejects_child() {
let plan = r#"
=== Plan
Root[id]
Read:Virtual[
- (1)
- => id:i64]
Read[t => x:i64]"#;
assert!(
Parser::parse(plan.trim()).is_err(),
"child relation under multi-line Read:Virtual should be rejected"
);
}
#[test]
fn test_virtual_read_multiline_rejects_stray_continuation() {
let plan = r#"
=== Plan
Root[id]
Read:Virtual[
- (1)
- => id:i64]
- (99)"#;
assert!(
Parser::parse(plan.trim()).is_err(),
"stray continuation line after Read:Virtual should be rejected"
);
}
#[test]
fn test_virtual_read_multiline_rejects_malformed_row() {
let plan = r#"
=== Plan
Root[id]
Read:Virtual[
- not_a_row
- => id:i64]"#;
assert!(
Parser::parse(plan.trim()).is_err(),
"malformed continuation row should fail to parse"
);
}
#[test]
fn test_virtual_read_two_rows_stays_inline() {
let inline = r#"
=== Plan
Root[id, name]
Read:Virtual[(1, 'alice'), (2, 'bob') => id:i64, name:string]"#;
let multiline = r#"
=== Plan
Root[id, name]
Read:Virtual[
- (1, 'alice'),
- (2, 'bob')
- => id:i64, name:string]"#;
assert_roundtrip_canonical(inline.trim(), multiline.trim());
}
#[test]
fn test_virtual_read_threshold_is_configurable() {
let inline = r#"
=== Plan
Root[id]
Read:Virtual[(1), (2), (3) => id:i64]"#;
let plan = Parser::parse(inline.trim()).expect("parse failed");
let options = OutputOptions {
virtual_table_multiline_threshold: 100,
..OutputOptions::default()
};
let (text, errors) = format_with_options(&plan, &options);
assert!(
errors.is_empty(),
"unexpected formatting errors: {errors:?}"
);
assert_eq!(text.trim(), inline.trim());
}
#[test]
fn test_virtual_read_empty_stays_inline_even_at_zero_threshold() {
let inline = r#"
=== Plan
Root[id, name]
Read:Virtual[_ => id:i64, name:string]"#;
let plan = Parser::parse(inline.trim()).expect("parse failed");
let options = OutputOptions {
virtual_table_multiline_threshold: 0,
..OutputOptions::default()
};
let (text, errors) = format_with_options(&plan, &options);
assert!(
errors.is_empty(),
"unexpected formatting errors: {errors:?}"
);
assert_eq!(text.trim(), inline.trim());
Parser::parse(text.trim()).expect("formatted empty virtual table should re-parse");
}