use proptest::prelude::*;
use std::collections::HashSet;
use triblespace_core::id::rngid;
use triblespace_core::inline::encodings::genid::GenId;
use triblespace_core::prelude::*;
use triblespace_core::query::{Constraint, VariableContext, VariableSet};
mod profile {
use triblespace_core::prelude::*;
attributes! {
"CC00000000000000DD00000000000001" unsafe as pub nickname: inlineencodings::ShortString;
"CC00000000000000DD00000000000002" unsafe as pub display_name: inlineencodings::ShortString;
"CC00000000000000DD00000000000003" unsafe as pub city: inlineencodings::ShortString;
"CC00000000000000DD00000000000010" unsafe as pub m0: inlineencodings::ShortString;
"CC00000000000000DD00000000000011" unsafe as pub m1: inlineencodings::ShortString;
"CC00000000000000DD00000000000012" unsafe as pub m2: inlineencodings::ShortString;
"CC00000000000000DD00000000000013" unsafe as pub m3: inlineencodings::ShortString;
"CC00000000000000DD00000000000014" unsafe as pub m4: inlineencodings::ShortString;
}
}
#[test]
fn book_nickname_display_name_example() {
let alice = rngid();
let bob = rngid();
let carol = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @ profile::nickname: "Ali" };
dataset += entity! { &bob @
profile::nickname: "Bobby",
profile::display_name: "Robert",
};
dataset += entity! { &carol @ profile::city: "Caladan" };
let mut aliases: Vec<String> = find!(
(alias: Inline<_>),
temp!(
(entity),
or!(
pattern!(&dataset, [{ ?entity @ profile::nickname: ?alias }]),
pattern!(&dataset, [{ ?entity @ profile::display_name: ?alias }])
)
)
)
.map(|(alias,)| alias.try_from_inline::<&str>().unwrap().to_string())
.collect();
aliases.sort_unstable();
assert_eq!(aliases, ["Ali", "Bobby", "Robert"]);
}
#[test]
fn or_arms_with_different_literals() {
let alice = rngid();
let bob = rngid();
let carol = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @ profile::nickname: "Ali" };
dataset += entity! { &bob @ profile::nickname: "Bobby" };
dataset += entity! { &carol @ profile::nickname: "Caro" };
let people: HashSet<[u8; 32]> = find!(
(person: Inline<GenId>),
or!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Ali" }]),
pattern!(&dataset, [{ ?person @ profile::nickname: "Bobby" }])
)
)
.map(|(person,)| person.raw)
.collect();
let alice_val: Inline<GenId> = (&alice).to_inline();
let bob_val: Inline<GenId> = (&bob).to_inline();
let expected: HashSet<[u8; 32]> = [alice_val.raw, bob_val.raw].into_iter().collect();
assert_eq!(people, expected);
}
#[test]
fn or_arms_over_different_sets() {
let alice = rngid();
let bob = rngid();
let mut set_a = TribleSet::new();
set_a += entity! { &alice @ profile::nickname: "Ali" };
let mut set_b = TribleSet::new();
set_b += entity! { &bob @ profile::nickname: "Bobby" };
let mut names: Vec<String> = find!(
(person: Inline<GenId>, name: Inline<_>),
or!(
pattern!(&set_a, [{ ?person @ profile::nickname: ?name }]),
pattern!(&set_b, [{ ?person @ profile::nickname: ?name }])
)
)
.map(|(_, name)| name.try_from_inline::<&str>().unwrap().to_string())
.collect();
names.sort_unstable();
assert_eq!(names, ["Ali", "Bobby"]);
}
#[test]
fn or_arms_mixed_leaf_and_composite() {
let ali_only = rngid();
let full_profile = rngid();
let display_only = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &ali_only @ profile::nickname: "Ali" };
dataset += entity! { &full_profile @
profile::nickname: "Bobby",
profile::display_name: "Robert",
};
dataset += entity! { &display_only @ profile::display_name: "Carola" };
let query = |flipped: bool| -> HashSet<[u8; 32]> {
let leaf_first = find!(
(person: Inline<GenId>),
or!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Ali" }]),
and!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Bobby" }]),
pattern!(&dataset, [{ ?person @ profile::display_name: "Robert" }])
)
)
);
let composite_first = find!(
(person: Inline<GenId>),
or!(
and!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Bobby" }]),
pattern!(&dataset, [{ ?person @ profile::display_name: "Robert" }])
),
pattern!(&dataset, [{ ?person @ profile::nickname: "Ali" }])
)
);
if flipped {
composite_first.map(|(p,)| p.raw).collect()
} else {
leaf_first.map(|(p,)| p.raw).collect()
}
};
let ali_val: Inline<GenId> = (&ali_only).to_inline();
let full_val: Inline<GenId> = (&full_profile).to_inline();
let expected: HashSet<[u8; 32]> = [ali_val.raw, full_val.raw].into_iter().collect();
assert_eq!(query(false), expected, "or!(leaf, composite)");
assert_eq!(
query(true),
expected,
"or!(composite, leaf) — arm order must not matter"
);
}
#[test]
fn pattern_constants_allocate_no_helper_variables() {
let alice = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @
profile::nickname: "Ali",
profile::city: "Caladan",
};
let mut ctx = VariableContext::new();
macro_rules! __local_find_context {
() => {
&mut ctx
};
}
let name = ctx.next_variable::<triblespace_core::prelude::inlineencodings::ShortString>();
let constraint = pattern!(&dataset, [{ &alice @
profile::nickname: ?name,
profile::city: "Caladan",
}]);
assert_eq!(
constraint.variables(),
VariableSet::new_singleton(name.index),
"constants must not appear in the constraint's variable set"
);
assert_eq!(
ctx.next_index, 1,
"no hidden variables may be allocated for constants \
(entity id, attribute constants, literal value)"
);
}
#[test]
fn constants_never_enter_binding() {
use triblespace_core::query::{Binding, Query};
let alice = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @
profile::nickname: "Ali",
profile::city: "Caladan",
};
let mut ctx = VariableContext::new();
macro_rules! __local_find_context {
() => {
&mut ctx
};
}
let name = ctx.next_variable::<triblespace_core::prelude::inlineencodings::ShortString>();
let constraint = pattern!(&dataset, [{ &alice @
profile::nickname: ?name,
profile::city: "Caladan",
}]);
let bound_sets: Vec<VariableSet> =
Query::new(constraint, |binding: &Binding| Some(binding.bound)).collect();
assert_eq!(bound_sets.len(), 1, "exactly one witness row expected");
assert_eq!(
bound_sets[0],
VariableSet::new_singleton(name.index),
"the result binding must contain the real variable and nothing else"
);
}
#[test]
fn many_literals_do_not_consume_the_variable_budget() {
let ids: Vec<_> = (0..26).map(|_| rngid()).collect();
let mut dataset = TribleSet::new();
for (i, id) in ids.iter().enumerate() {
dataset += entity! { id @
profile::m0: format!("v{}_0", i).as_str(),
profile::m1: format!("v{}_1", i).as_str(),
profile::m2: format!("v{}_2", i).as_str(),
profile::m3: format!("v{}_3", i).as_str(),
profile::m4: format!("v{}_4", i).as_str(),
};
}
let mut ctx = VariableContext::new();
macro_rules! __local_find_context {
() => {
&mut ctx
};
}
let constraint = pattern!(&dataset, [
{ &ids[0] @ profile::m0: "v0_0", profile::m1: "v0_1", profile::m2: "v0_2", profile::m3: "v0_3", profile::m4: "v0_4" },
{ &ids[1] @ profile::m0: "v1_0", profile::m1: "v1_1", profile::m2: "v1_2", profile::m3: "v1_3", profile::m4: "v1_4" },
{ &ids[2] @ profile::m0: "v2_0", profile::m1: "v2_1", profile::m2: "v2_2", profile::m3: "v2_3", profile::m4: "v2_4" },
{ &ids[3] @ profile::m0: "v3_0", profile::m1: "v3_1", profile::m2: "v3_2", profile::m3: "v3_3", profile::m4: "v3_4" },
{ &ids[4] @ profile::m0: "v4_0", profile::m1: "v4_1", profile::m2: "v4_2", profile::m3: "v4_3", profile::m4: "v4_4" },
{ &ids[5] @ profile::m0: "v5_0", profile::m1: "v5_1", profile::m2: "v5_2", profile::m3: "v5_3", profile::m4: "v5_4" },
{ &ids[6] @ profile::m0: "v6_0", profile::m1: "v6_1", profile::m2: "v6_2", profile::m3: "v6_3", profile::m4: "v6_4" },
{ &ids[7] @ profile::m0: "v7_0", profile::m1: "v7_1", profile::m2: "v7_2", profile::m3: "v7_3", profile::m4: "v7_4" },
{ &ids[8] @ profile::m0: "v8_0", profile::m1: "v8_1", profile::m2: "v8_2", profile::m3: "v8_3", profile::m4: "v8_4" },
{ &ids[9] @ profile::m0: "v9_0", profile::m1: "v9_1", profile::m2: "v9_2", profile::m3: "v9_3", profile::m4: "v9_4" },
{ &ids[10] @ profile::m0: "v10_0", profile::m1: "v10_1", profile::m2: "v10_2", profile::m3: "v10_3", profile::m4: "v10_4" },
{ &ids[11] @ profile::m0: "v11_0", profile::m1: "v11_1", profile::m2: "v11_2", profile::m3: "v11_3", profile::m4: "v11_4" },
{ &ids[12] @ profile::m0: "v12_0", profile::m1: "v12_1", profile::m2: "v12_2", profile::m3: "v12_3", profile::m4: "v12_4" },
{ &ids[13] @ profile::m0: "v13_0", profile::m1: "v13_1", profile::m2: "v13_2", profile::m3: "v13_3", profile::m4: "v13_4" },
{ &ids[14] @ profile::m0: "v14_0", profile::m1: "v14_1", profile::m2: "v14_2", profile::m3: "v14_3", profile::m4: "v14_4" },
{ &ids[15] @ profile::m0: "v15_0", profile::m1: "v15_1", profile::m2: "v15_2", profile::m3: "v15_3", profile::m4: "v15_4" },
{ &ids[16] @ profile::m0: "v16_0", profile::m1: "v16_1", profile::m2: "v16_2", profile::m3: "v16_3", profile::m4: "v16_4" },
{ &ids[17] @ profile::m0: "v17_0", profile::m1: "v17_1", profile::m2: "v17_2", profile::m3: "v17_3", profile::m4: "v17_4" },
{ &ids[18] @ profile::m0: "v18_0", profile::m1: "v18_1", profile::m2: "v18_2", profile::m3: "v18_3", profile::m4: "v18_4" },
{ &ids[19] @ profile::m0: "v19_0", profile::m1: "v19_1", profile::m2: "v19_2", profile::m3: "v19_3", profile::m4: "v19_4" },
{ &ids[20] @ profile::m0: "v20_0", profile::m1: "v20_1", profile::m2: "v20_2", profile::m3: "v20_3", profile::m4: "v20_4" },
{ &ids[21] @ profile::m0: "v21_0", profile::m1: "v21_1", profile::m2: "v21_2", profile::m3: "v21_3", profile::m4: "v21_4" },
{ &ids[22] @ profile::m0: "v22_0", profile::m1: "v22_1", profile::m2: "v22_2", profile::m3: "v22_3", profile::m4: "v22_4" },
{ &ids[23] @ profile::m0: "v23_0", profile::m1: "v23_1", profile::m2: "v23_2", profile::m3: "v23_3", profile::m4: "v23_4" },
{ &ids[24] @ profile::m0: "v24_0", profile::m1: "v24_1", profile::m2: "v24_2", profile::m3: "v24_3", profile::m4: "v24_4" },
{ &ids[25] @ profile::m0: "v25_0", profile::m1: "v25_1", profile::m2: "v25_2", profile::m3: "v25_3", profile::m4: "v25_4" },
]);
assert_eq!(
ctx.next_index, 0,
"161 constants must allocate zero query variables"
);
assert_eq!(
constraint.variables(),
VariableSet::new_empty(),
"a fully-constant pattern has an empty variable set"
);
assert!(
exists!(constraint),
"the fully-constant pattern must verify as an existence check"
);
}
#[test]
fn fully_constant_pattern_is_an_existence_check() {
let alice = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @ profile::nickname: "Ali" };
assert!(exists!(
pattern!(&dataset, [{ &alice @ profile::nickname: "Ali" }])
));
assert!(!exists!(
pattern!(&dataset, [{ &alice @ profile::nickname: "Bobby" }])
));
assert!(!exists!(
pattern!(&dataset, [{ &alice @ profile::display_name: "Ali" }])
));
}
#[test]
fn fully_constant_pattern_composes_with_and() {
let alice = rngid();
let bob = rngid();
let mut dataset = TribleSet::new();
dataset += entity! { &alice @ profile::nickname: "Ali" };
dataset += entity! { &bob @ profile::nickname: "Bobby" };
let live: Vec<_> = find!(
(person: Inline<GenId>),
and!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Bobby" }]),
pattern!(&dataset, [{ &alice @ profile::nickname: "Ali" }])
)
)
.collect();
assert_eq!(live.len(), 1, "a satisfied constant pattern is a tautology");
let dead: Vec<_> = find!(
(person: Inline<GenId>),
and!(
pattern!(&dataset, [{ ?person @ profile::nickname: "Bobby" }]),
pattern!(&dataset, [{ &alice @ profile::nickname: "NotAli" }])
)
)
.collect();
assert!(
dead.is_empty(),
"a failed constant pattern must empty the conjunction"
);
}
#[test]
#[should_panic(expected = "must mention the same query variables")]
fn or_panics_when_arms_mention_different_variables() {
let dataset = TribleSet::new();
let _ = find!(
(alias: Inline<_>),
temp!(
(x, y),
or!(
pattern!(&dataset, [{ ?x @ profile::nickname: ?alias }]),
pattern!(&dataset, [{ ?y @ profile::display_name: ?alias }])
)
)
)
.count();
}
proptest! {
#[test]
fn or_equals_union_oracle(
assignments in proptest::collection::vec(
(0usize..6, prop_oneof![Just(0u8), Just(1u8)], 0usize..4),
0..24,
)
) {
let entities: Vec<_> = (0..6).map(|_| rngid()).collect();
let values = ["v0", "v1", "v2", "v3"];
let mut dataset = TribleSet::new();
for (e, which, v) in &assignments {
let entity = &entities[*e];
dataset += match which {
0 => entity! { entity @ profile::nickname: values[*v] },
_ => entity! { entity @ profile::display_name: values[*v] },
};
}
let nick_rows: HashSet<([u8; 32], [u8; 32])> = find!(
(person: Inline<GenId>, alias: Inline<_>),
pattern!(&dataset, [{ ?person @ profile::nickname: ?alias }])
)
.map(|(p, a)| (p.raw, a.raw))
.collect();
let disp_rows: HashSet<([u8; 32], [u8; 32])> = find!(
(person: Inline<GenId>, alias: Inline<_>),
pattern!(&dataset, [{ ?person @ profile::display_name: ?alias }])
)
.map(|(p, a)| (p.raw, a.raw))
.collect();
let oracle: HashSet<_> = nick_rows.union(&disp_rows).copied().collect();
let forward: HashSet<([u8; 32], [u8; 32])> = find!(
(person: Inline<GenId>, alias: Inline<_>),
or!(
pattern!(&dataset, [{ ?person @ profile::nickname: ?alias }]),
pattern!(&dataset, [{ ?person @ profile::display_name: ?alias }])
)
)
.map(|(p, a)| (p.raw, a.raw))
.collect();
let backward: HashSet<([u8; 32], [u8; 32])> = find!(
(person: Inline<GenId>, alias: Inline<_>),
or!(
pattern!(&dataset, [{ ?person @ profile::display_name: ?alias }]),
pattern!(&dataset, [{ ?person @ profile::nickname: ?alias }])
)
)
.map(|(p, a)| (p.raw, a.raw))
.collect();
prop_assert_eq!(&forward, &oracle, "or! must equal the set-union oracle");
prop_assert_eq!(&backward, &oracle, "or! must be arm-order independent");
}
}