use quarb_csv::CsvAdapter;
const DOC: &str = "\
name,dept,age,salary
Ada,eng,36,120000
Bo,sales,17,45000
Cy,eng,64,95000
Dee,\"ops, misc\",41,
";
fn nodes(query: &str) -> Vec<String> {
let adapter = CsvAdapter::parse(DOC).unwrap();
match quarb::run(query, &adapter).unwrap() {
quarb::QueryResult::Nodes(ns) => ns.into_iter().map(|n| adapter.locator(n)).collect(),
quarb::QueryResult::Values(_) => panic!("expected nodes"),
}
}
fn values(query: &str) -> Vec<String> {
let adapter = CsvAdapter::parse(DOC).unwrap();
match quarb::run(query, &adapter).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
quarb::QueryResult::Nodes(_) => panic!("expected values"),
}
}
#[test]
fn rows_and_columns() {
assert_eq!(nodes("/row").len(), 4);
assert_eq!(values("//row @| count"), vec!["4"]);
assert_eq!(nodes("/row[2]"), vec!["/row[2]"]);
assert_eq!(nodes("/row[-1]"), vec!["/row[4]"]);
assert_eq!(values("/row[1]::name"), vec!["Ada"]);
assert_eq!(values("/row[4]::dept"), vec!["ops, misc"]);
assert_eq!(values("^:::n-rows"), vec!["4"]);
assert_eq!(values("^:::columns"), vec!["name, dept, age, salary"]);
}
#[test]
fn filtering_and_arithmetic() {
assert_eq!(values("/row[::age > 35]::name"), vec!["Ada", "Cy", "Dee"]);
assert_eq!(
values(r#"/row[::dept = "eng" && ::age < 40]::name"#),
vec!["Ada"]
);
assert_eq!(values("/row[::salary div ::age > 3000]::name"), vec!["Ada"]);
assert_eq!(
values("/row[::dept = \"eng\"] | .(::salary + 0) | $. @| sum"),
vec!["215000"]
);
}
#[test]
fn missing_values() {
assert_eq!(values("/row[::name = \"Dee\"]::salary"), vec![""]);
assert_eq!(values("/row::salary @| mean"), vec!["86666.66666666667"]);
assert_eq!(values("/row::salary @| count"), vec!["4"]);
assert_eq!(values("/row[::salary]::name @| count"), vec!["3"]);
assert_eq!(values("/row[::name = \"Dee\"] | ::salary * 2"), vec![""]);
assert_eq!(
values("/row[::name = \"Dee\"] | ::salary | default(0)"),
vec!["0"]
);
assert_eq!(values("/row[1] | ::salary | default(0)"), vec!["120000"]);
}
#[test]
fn aggregation() {
assert_eq!(values("/row::age @| median"), vec!["38.5"]);
assert_eq!(values("/row::age @| max"), vec!["64"]);
assert_eq!(values("/row::name @| sort @| first"), vec!["Ada"]);
assert_eq!(values("/row[::dept = \"eng\"]::age @| mean"), vec!["50"]);
}
#[test]
fn keyed_aggregates() {
assert_eq!(values("/row @| max_by(::salary) | ::name"), vec!["Ada"]);
assert_eq!(values("/row @| min_by(::salary) | ::name"), vec!["Bo"]);
assert_eq!(values("/row @| top(2, ::age) | ::name"), vec!["Cy", "Dee"]);
assert_eq!(values("/row @| bottom(1, ::age) | ::name"), vec!["Bo"]);
assert_eq!(
values("/row @| sort_by(::salary) | ::name"),
vec!["Bo", "Cy", "Ada", "Dee"]
);
assert_eq!(
values("/row @| unique_by(::dept) | ::dept"),
vec!["eng", "sales", "ops, misc"]
);
assert_eq!(
values("/row @| sort_by(::age) @| [..2] | ::name"),
vec!["Bo", "Ada"]
);
assert_eq!(nodes("/row @| max_by(::age)"), vec!["/row[3]"]);
}
#[test]
fn records_and_json() {
assert_eq!(
values(r#"/row[::dept = "eng"] | %(::name; monthly = ::salary div 12)"#),
vec![
"%(name = \"Ada\"; monthly = 10000)",
"%(name = \"Cy\"; monthly = 7916.666666666667)"
]
);
assert_eq!(values("/row[1]::name | json"), vec![r#""Ada""#]);
assert_eq!(values("/row[-1]::salary | json"), vec!["null"]);
assert_eq!(
values("/row @| max_by(::salary) | %(::name; ::salary)"),
vec!["%(name = \"Ada\"; salary = \"120000\")"]
);
}
#[test]
fn grouping() {
assert_eq!(
values("/row @| group(::dept) | count | %($.dept; n = $_)"),
vec![
"%(dept = \"eng\"; n = 2)",
"%(dept = \"sales\"; n = 1)",
"%(dept = \"ops, misc\"; n = 1)"
]
);
assert_eq!(
values(
"/row::salary @| group(::dept) | mean | .m | %($.dept; mean = $.m) \
@| sort_by($.m)"
),
vec![
"%(dept = \"sales\"; mean = 45000)",
"%(dept = \"eng\"; mean = 107500)",
"%(dept = \"ops, misc\"; mean = null)"
]
);
assert_eq!(
values("/row @| group(\"senior\", ::age idiv 40) | count | %($.senior; n = $_)"),
vec!["%(senior = 0; n = 2)", "%(senior = 1; n = 2)"]
);
assert_eq!(
values("/row @| group(::salary) | count @| count"),
vec!["3"]
);
}
#[test]
fn group_members() {
assert_eq!(
values(
"/row @| group(::dept) | top(1, ::salary) @| ungroup \
| %($.dept; ::name; ::salary)"
),
vec![
"%(dept = \"eng\"; name = \"Ada\"; salary = \"120000\")",
"%(dept = \"sales\"; name = \"Bo\"; salary = \"45000\")",
"%(dept = \"ops, misc\"; name = \"Dee\"; salary = null)"
]
);
assert_eq!(
values("/row::salary @| group(::dept) | top(1, $_) | sum | %($.dept; max = $_)"),
vec![
"%(dept = \"eng\"; max = 120000)",
"%(dept = \"sales\"; max = 45000)",
"%(dept = \"ops, misc\"; max = null)"
]
);
assert_eq!(
nodes("/row @| group(::dept) | top(1, ::age) @| ungroup"),
vec!["/row[3]", "/row[2]", "/row[4]"]
);
assert_eq!(
values("/row::name @| group(::dept) | top(1, ::age) @| ungroup"),
vec!["Cy", "Bo", "Dee"]
);
let adapter = CsvAdapter::parse(DOC).unwrap();
let e = quarb::run("/row | top(1, ::age) @| ungroup @| count", &adapter).unwrap_err();
assert!(e.to_string().contains("ranks a context"), "{e}");
}
#[test]
fn ordinal() {
assert_eq!(
values("/row @| sort_by(::age) | %($ord; ::name)"),
vec![
"%(ordinal = 1; name = \"Bo\")",
"%(ordinal = 2; name = \"Ada\")",
"%(ordinal = 3; name = \"Dee\")",
"%(ordinal = 4; name = \"Cy\")"
]
);
assert_eq!(
values("/row | [$ord mod 2 = 1] | ::name"),
vec!["Ada", "Cy"]
);
assert_eq!(
values("/row | [::dept = \"eng\"] | $ord + 0"),
vec!["1", "2"]
);
assert_eq!(values("/row[$ord = 1] @| count"), vec!["0"]);
}
#[test]
fn windows() {
assert_eq!(
values("/row | ::age @| window(-1..0) | sum"),
vec!["36", "53", "81", "105"]
);
assert_eq!(
values("/row | ::age @| window(2) | sum"),
vec!["36", "53", "81", "105"]
);
assert_eq!(
values("/row | ::age @| window(..0) | sum"),
vec!["36", "53", "117", "158"]
);
assert_eq!(
values("/row @| window(-1..1) | count"),
vec!["2", "3", "3", "2"]
);
assert_eq!(
values("/row | ::age @| window(0..1) | max"),
vec!["36", "64", "64", "41"]
);
assert_eq!(
values("/row @| window(..-1) | count"),
vec!["0", "1", "2", "3"]
);
assert_eq!(
values("/row | ::age @| window(-1..0, ::dept) | sum"),
vec!["36", "17", "100", "41"]
);
assert_eq!(
values("/row | ::age @| window(-1..0) | top(1, $_) | sum"),
vec!["36", "36", "64", "64"]
);
assert_eq!(
values("/row | ::age | .a @| window(-1..0) | sum | $_ - $.a"),
vec!["0", "36", "17", "64"]
);
}
#[test]
fn shift() {
assert_eq!(
values("/row | ::age @| shift(1)"),
vec!["", "36", "17", "64"]
);
assert_eq!(
values("/row | ::age @| shift(-1)"),
vec!["17", "64", "41", ""]
);
assert_eq!(
values("/row | ::age | .now @| shift(1) | $.now - $_"),
vec!["", "-19", "47", "-23"]
);
assert_eq!(
values("/row | ::age @| shift(1, ::dept)"),
vec!["", "", "36", ""]
);
}
#[test]
fn interpolation() {
assert_eq!(
values(r#"/row[..2] | "${::name} is ${::age}""#),
vec!["Ada is 36", "Bo is 17"]
);
assert_eq!(
values(r#"/row[1] | ::salary | .s | "\$${$.s}, monthly \$${$_ idiv 12}""#),
vec!["$120000, monthly $10000"]
);
assert_eq!(values(r#"/row[-1] | "[${::salary}]""#), vec!["[]"]);
assert_eq!(values(r#"/row[::dept = "eng"] @| count"#), vec!["2"]);
assert_eq!(
values(r#"/row[1] | %(who = "${::name}/${::dept}")"#),
vec!["%(who = \"Ada/eng\")"]
);
}
#[test]
fn root_anchor_in_subcontexts() {
assert_eq!(
values("/row @| group(::dept) | count | .n | .t(^/row @| count) | $.n * 100 idiv $.t"),
vec!["50", "25", "25"]
);
assert_eq!(values("^/row @| count"), vec!["4"]);
}
#[test]
fn macros_end_to_end() {
assert_eq!(
values(
r#"macro &describe(@cols):
/cols/* | .k(/projection::key)
| "| .${$.k}-mean(/row${::form} @| mean)";
^ | &describe(::age, ::salary) | %."#
),
vec!["%(\"age-mean\" = 39.5; \"salary-mean\" = 86666.66666666667)"]
);
assert_eq!(
values(r#"macro &odd($n): ^ | "| [\$ord mod ${$n} = 1]"; /row | &odd(2) | ::name"#),
vec!["Ada", "Cy"]
);
}
#[test]
fn data_aware_macros() {
assert_eq!(
values(
r#"macro &pivot!($col):
/data/row[::age < 40] | $col @| unique
| "| .n-${$_}(/row[${$col} = \"${$_}\"] @| count)";
^ | &pivot!(::dept) | %."#
),
vec!["%(\"n-eng\" = 2; \"n-sales\" = 1)"]
);
assert_eq!(
values(
r#"macro &max!($col):
/data/row | $col @| max | "| \"${$_}\"";
^ | &max!(::age)"#
),
vec!["64"]
);
}
#[test]
fn window_shapes() {
let err = |q: &str| {
let adapter = CsvAdapter::parse(DOC).unwrap();
quarb::run(q, &adapter).unwrap_err().to_string()
};
assert!(err("/row | ::age | window(-1..0)").contains("'window' uses '@|'"));
assert!(err("/row | ::age | shift(1)").contains("'shift' uses '@|'"));
assert!(err("/row @| window(2..1)").contains("start <= end"));
assert!(err("/row @| window(0)").contains("offset range or a count"));
assert!(err("/row @| shift()").contains("integer distance"));
assert!(err("/row @| sum(1..2)").contains("no range argument"));
let adapter = CsvAdapter::parse(DOC).unwrap();
assert!(quarb::run("/row @| top(1..2; ::age)", &adapter).is_ok());
}
#[test]
fn the_fields_push_takes_a_record() {
let adapter = CsvAdapter::parse(DOC).unwrap();
let err = quarb::run("/row | ::age | .% | %.", &adapter)
.unwrap_err()
.to_string();
assert!(
err.contains("the fields push '| .%' takes a record topic, not text"),
"{err}"
);
let err = quarb::run("/row | .% | %.", &adapter)
.unwrap_err()
.to_string();
assert!(err.contains("not a node with no topic"), "{err}");
}
#[test]
fn register_record_view() {
assert_eq!(
values("/row[::age > 35] | .name(::name) | .age(::age) | %."),
vec![
"%(name = \"Ada\"; age = \"36\")",
"%(name = \"Cy\"; age = \"64\")",
"%(name = \"Dee\"; age = \"41\")"
]
);
assert_eq!(
values("/row @| group(::dept) | count | .n | %."),
vec![
"%(dept = \"eng\"; n = 2)",
"%(dept = \"sales\"; n = 1)",
"%(dept = \"ops, misc\"; n = 1)"
]
);
assert_eq!(
values("/row[1] | .a(::name) | ::dept | . | .b(::age) | .a(::salary) | %."),
vec!["%(a = \"120000\"; b = \"36\")"]
);
assert_eq!(values("/row[1] | %."), vec!["%()"]);
}
#[test]
fn fragments() {
assert_eq!(
values("def &grown: /row[::age >= 18]; &grown @| count"),
vec!["3"]
);
assert_eq!(
values("def &above($col, $min): /row[$col >= $min]; &above(::salary, 100000) | ::name"),
vec!["Ada"]
);
assert_eq!(
values(
"def &cols: | .name(::name) | .dept(::dept) | %.; \
/row @| top(1, ::salary) | &cols"
),
vec!["%(name = \"Ada\"; dept = \"eng\")"]
);
assert_eq!(
values(
"def &grown: /row[::age >= 18]; \
def &grown-eng: &grown | [::dept = \"eng\"]; \
&grown-eng | ::name"
),
vec!["Ada", "Cy"]
);
assert_eq!(
values("def &col($c): | .v($c) | $.v; /row | &col(::age * 2) @| max"),
vec!["128"]
);
let defs = quarb::parse_defs("def &grown: /row[::age >= 18];").unwrap();
let adapter = CsvAdapter::parse(DOC).unwrap();
match quarb::run_with_defs("&grown @| count", &defs, &adapter).unwrap() {
quarb::QueryResult::Values(vs) => assert_eq!(vs[0].to_string(), "3"),
_ => panic!("expected values"),
}
}
#[test]
fn captures() {
assert_eq!(
values(r#"/row | [::dept == (/^(\w+)s$/)] | %(stem = $1; ::name)"#),
vec!["%(stem = \"sale\"; name = \"Bo\")"]
);
assert_eq!(
values(r#"/row | [::name == (/^(\w)/)] | .initial($1) | .age(::age) | %. @| [1..2]"#),
vec![
"%(initial = \"A\"; age = \"36\")",
"%(initial = \"B\"; age = \"17\")"
]
);
assert_eq!(
values(r#"/row | [::dept == (/^(\w)/)] @| group("initial", $1) | count | .n | %."#),
vec![
"%(initial = \"e\"; n = 2)",
"%(initial = \"s\"; n = 1)",
"%(initial = \"o\"; n = 1)"
]
);
assert_eq!(
values(r#"/row | [::name == (/^(\w+)$/)] | [::age > 30] | %(who = $1; g2 = $2)"#),
vec![
"%(who = \"Ada\"; g2 = null)",
"%(who = \"Cy\"; g2 = null)",
"%(who = \"Dee\"; g2 = null)"
]
);
assert_eq!(values("/row[1] | %(c = $1)"), vec!["%(c = null)"]);
}
#[test]
fn property_at_root_is_null() {
use quarb::{AstAdapter, NodeId};
let adapter = CsvAdapter::parse(DOC).unwrap();
assert!(adapter.property(NodeId(0), "age").is_none());
}
#[test]
fn tsv_and_errors() {
let tsv = "a\tb\n1\t2\n";
let adapter = CsvAdapter::parse_with_delimiter(tsv, b'\t').unwrap();
match quarb::run("/row::b", &adapter).unwrap() {
quarb::QueryResult::Values(vs) => assert_eq!(vs.len(), 1),
_ => panic!("expected values"),
}
assert!(CsvAdapter::parse("a,b\n1,2,3\n").is_err());
}
#[test]
fn correlated_subqueries() {
assert_eq!(
values(
"/row @| group(::dept) \
| .adults(^/row[::dept = $.dept][::age >= 18] @| count) | %."
),
vec![
"%(dept = \"eng\"; adults = 2)",
"%(dept = \"sales\"; adults = 0)",
"%(dept = \"ops, misc\"; adults = 1)"
]
);
assert_eq!(
values(
"/row @| [..2] | .d(::dept) \
| .m(^/row[::dept = $.d]::salary @| mean) | $.m"
),
vec!["107500", "45000"]
);
assert_eq!(
values("/row @| [..2] | ::age | .t(^/row[:index = $$__:index]::name) | $.t"),
vec!["Ada", "Bo"]
);
assert_eq!(values("/row[1] | $.x"), vec![""]);
assert_eq!(
values(
"/row[1] | .a(::age) | .x(^/row[1] | .b(::dept) | .y(^/row[::age = $.a] @| count) | $.y) | $.x"
),
vec!["1"]
);
}
#[test]
fn arrows_are_refused_where_nothing_refers() {
let adapter = CsvAdapter::parse(DOC).unwrap();
for q in ["/*[1]::dept-->", "/*[1]<--(/*::dept)", "/*[1]<--"] {
let err = quarb::run(q, &adapter).unwrap_err().to_string();
assert!(err.contains("defines none"), "{q}: {err}");
}
assert_eq!(values("/*[1]::dept=>(/*::dept)::name"), ["Ada", "Cy"]);
}