1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! Free-attribute support in `pattern!` (`{ ?e @ ?attr: ?val }`).
//!
//! When the predicate slot of a pattern is a query variable, the
//! macro can't apply an attribute-specific schema cast to the
//! value. To prevent users from accidentally misinterpreting the
//! result bytes, the macro requires the value variable to be
//! typed `Variable<UnknownValue>` and emits a compile-time type
//! assertion that fails to compile if the user picks any other
//! schema. The bytes come back as opaque 32-byte handles; turning
//! them into something typed is an explicit
//! `try_from_value::<RealSchema>()` step the receiver makes
//! at the use site, where they know which predicate's bytes
//! they're holding.
//!
//! This is the building block for SPB-style
//! `?cw ?pred ?value` projections (Q3 / Q4 outer CONSTRUCT) and
//! for general-purpose schema-erased iteration over an entity.
use std::collections::HashSet;
use triblespace::prelude::valueschemas::ShortString;
use triblespace::prelude::*;
mod ns {
use triblespace::prelude::*;
attributes! {
"1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A" as name: valueschemas::ShortString;
"2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B" as friend: valueschemas::GenId;
"3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C" as nickname: valueschemas::ShortString;
}
}
#[test]
fn free_attribute_enumerates_predicates_for_fixed_entity() {
// Build a tiny graph: alice has name + friend + nickname.
// Bob is the friend target.
let mut set = TribleSet::new();
let alice = fucid();
let bob = fucid();
set += entity! { &alice @
ns::name: "alice",
ns::friend: &bob,
ns::nickname: "ali",
};
// Free-attribute pattern: ask for every (predicate, value) on
// alice. The receiver gets the predicate Id and an opaque
// UnknownValue for each pair.
let mut seen: HashSet<Id> = HashSet::new();
for (attr, _val) in find!(
(attr: Id, val: Value<UnknownValue>),
pattern!(&set, [{ alice.id @ ?attr: ?val }])
) {
seen.insert(attr);
}
assert!(seen.contains(&ns::name.id()));
assert!(seen.contains(&ns::friend.id()));
assert!(seen.contains(&ns::nickname.id()));
assert_eq!(seen.len(), 3);
}
#[test]
fn free_attribute_filters_with_external_predicate_check() {
// Same graph, but exclude the `friend` predicate at the user
// level — same shape as SPB Q3's
// FILTER (?pred NOT IN (cwork:tag, cwork:about, cwork:mention)).
let mut set = TribleSet::new();
let alice = fucid();
let bob = fucid();
set += entity! { &alice @
ns::name: "alice",
ns::friend: &bob,
ns::nickname: "ali",
};
let excluded = [ns::friend.id()];
let kept: Vec<Id> = find!(
(attr: Id, val: Value<UnknownValue>),
pattern!(&set, [{ alice.id @ ?attr: ?val }])
)
.map(|(attr, _val)| attr)
.filter(|a| !excluded.contains(a))
.collect();
let kept_set: HashSet<Id> = kept.into_iter().collect();
assert_eq!(kept_set.len(), 2);
assert!(kept_set.contains(&ns::name.id()));
assert!(kept_set.contains(&ns::nickname.id()));
assert!(!kept_set.contains(&ns::friend.id()));
}
#[test]
fn free_attribute_with_free_entity_enumerates_full_index() {
// Two entities, three (e, a, v) tribles total. With both
// entity and attribute free we should enumerate every trible
// in the set.
let mut set = TribleSet::new();
let alice = fucid();
let bob = fucid();
set += entity! { &alice @ ns::name: "alice", ns::nickname: "ali" };
set += entity! { &bob @ ns::name: "bob" };
let triples: Vec<(Id, Id)> = find!(
(e: Id, a: Id, v: Value<UnknownValue>),
pattern!(&set, [{ ?e @ ?a: ?v }])
)
.map(|(e, a, _v)| (e, a))
.collect();
assert_eq!(triples.len(), 3);
}
#[test]
fn free_attribute_value_is_byte_addressable() {
// The value comes back as `Value<UnknownValue>` — 32 raw
// bytes. We can compare bytes with a known schema value to
// confirm the lookup is faithful.
let mut set = TribleSet::new();
let alice = fucid();
set += entity! { &alice @ ns::name: "alice" };
let expected: Value<ShortString> = ShortString::value_from("alice".to_string());
let mut found: Option<Value<UnknownValue>> = None;
for (_attr, val) in find!(
(attr: Id, val: Value<UnknownValue>),
pattern!(&set, [{ alice.id @ ?attr: ?val }])
) {
found = Some(val);
}
let val = found.expect("one binding");
assert_eq!(val.raw, expected.raw);
}
#[test]
fn local_free_attribute_with_projected_value() {
// `_?attr` (pattern-local) in the attribute slot is supported
// when the value is a projected `Variable<UnknownValue>`.
// Local helper vars in the value slot of a free-attr pattern
// are not supported (no schema can be inferred); use `?val`
// (a find!-projected Variable<UnknownValue>) instead.
let mut set = TribleSet::new();
let alice = fucid();
let bob = fucid();
set += entity! { &alice @ ns::name: "alice", ns::friend: &bob };
let rows: Vec<Value<UnknownValue>> = find!(
(val: Value<UnknownValue>),
pattern!(&set, [{ alice.id @ _?a: ?val }])
)
.map(|(v,)| v)
.collect();
// Should see two rows (one per attribute on alice).
assert_eq!(rows.len(), 2);
let _ = bob;
}