Skip to content

Commit 02617f6

Browse files
committed
answer history len and contains server-side
len() and `in` on the remote history and its sub-collections downloaded the entire history to answer a scalar question. The server's History type gains contains (by timestamp, optionally with the event id), and the timestamps, event-id and intervals sub-views gain count and contains; the client asks those instead. Membership takes whatever converts to an EventTime, exactly as locally — a bare int means (t, event_id=0), and a naive datetime is not a member and never reaches the wire.
1 parent d81fdcf commit 02617f6

7 files changed

Lines changed: 309 additions & 39 deletions

File tree

python/tests/test_base_install/test_graphql/parity/test_parity_rpc_counts.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,36 @@ def test_list_builtin_adds_the_length_hint_len_rpc(counted):
702702
),
703703
"history.t.len": (lambda rg: rg.node("a").history.t, lambda t: len(t), 1),
704704
"history.t.contains": (lambda rg: rg.node("a").history.t, lambda t: 1 in t, 1),
705+
"history.event_id.contains": (
706+
lambda rg: rg.node("a").history.event_id,
707+
lambda e: 0 in e,
708+
1,
709+
),
710+
"history.intervals.len": (
711+
lambda rg: rg.node("a").history.intervals,
712+
lambda i: len(i),
713+
1,
714+
),
715+
"history.intervals.contains": (
716+
lambda rg: rg.node("a").history.intervals,
717+
lambda i: 1 in i,
718+
1,
719+
),
720+
"history.dt.contains": (
721+
lambda rg: rg.node("a").history.dt,
722+
lambda d: __import__("datetime").datetime(
723+
1970, 1, 1, tzinfo=__import__("datetime").timezone.utc
724+
)
725+
in d,
726+
1,
727+
),
728+
# A naive datetime is not UTC-convertible, so it is simply not a member —
729+
# answered client-side with no wire trip at all.
730+
"history.dt.contains_naive": (
731+
lambda rg: rg.node("a").history.dt,
732+
lambda d: __import__("datetime").datetime(1970, 1, 1) in d,
733+
0,
734+
),
705735
"nodes.len": (lambda rg: rg.nodes, lambda ns: len(ns), 1),
706736
"nodes.bool": (lambda rg: rg.nodes, lambda ns: bool(ns), 1),
707737
"edges.len": (lambda rg: rg.edges, lambda es: len(es), 1),

raphtory-graphql/schema.graphql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3036,6 +3036,12 @@ type History {
30363036
"""
30373037
count: Int!
30383038
"""
3039+
Whether an entry equal to the given time is present. With `eventId`,
3040+
an entry must match both the timestamp and the event id; without it,
3041+
any entry at the timestamp matches.
3042+
"""
3043+
contains(timestamp: Int!, eventId: Int): Boolean!
3044+
"""
30393045
Returns a HistoryTimestamp object which accesses timestamps (milliseconds since the Unix epoch)
30403046
instead of EventTime entries.
30413047
"""
@@ -3152,6 +3158,14 @@ type HistoryDateTime {
31523158
History object that provides access to event ids instead of `EventTime` entries.
31533159
"""
31543160
type HistoryEventId {
3161+
"""
3162+
Get the number of event ids (one per entry).
3163+
"""
3164+
count: Int!
3165+
"""
3166+
Whether the given value is present among the event ids.
3167+
"""
3168+
contains(value: Int!): Boolean!
31553169
"""
31563170
List event ids.
31573171
"""
@@ -3208,6 +3222,14 @@ type HistoryEventId {
32083222
History object that provides access to timestamps (milliseconds since the Unix epoch) instead of `EventTime` entries.
32093223
"""
32103224
type HistoryTimestamp {
3225+
"""
3226+
Get the number of timestamps (one per entry).
3227+
"""
3228+
count: Int!
3229+
"""
3230+
Whether the given value is present among the timestamps.
3231+
"""
3232+
contains(value: Int!): Boolean!
32113233
"""
32123234
List all timestamps.
32133235
"""
@@ -3275,6 +3297,15 @@ input InputEdge {
32753297
Provides access to the intervals between temporal entries of an object.
32763298
"""
32773299
type Intervals {
3300+
"""
3301+
Get the number of intervals (one per consecutive pair of entries,
3302+
so one less than the history's count; zero for an empty history).
3303+
"""
3304+
count: Int!
3305+
"""
3306+
Whether the given value is present among the intervals.
3307+
"""
3308+
contains(value: Int!): Boolean!
32783309
"""
32793310
List time intervals between consecutive timestamps in milliseconds.
32803311
"""

raphtory-graphql/src/client/graphql_transport.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,24 @@ fn render_read_into(
11051105
render_read_into(input, vars, out)?;
11061106
out.push_str(" { reverse");
11071107
}
1108+
ReadExpr::HistoryContains {
1109+
input,
1110+
timestamp,
1111+
event_id,
1112+
} => {
1113+
render_read_into(input, vars, out)?;
1114+
match event_id {
1115+
Some(event_id) => write!(
1116+
out,
1117+
" {{ contains(timestamp: {timestamp}, eventId: {event_id})"
1118+
)?,
1119+
None => write!(out, " {{ contains(timestamp: {timestamp})")?,
1120+
}
1121+
}
1122+
ReadExpr::HistoryValueContains { input, value } => {
1123+
render_read_into(input, vars, out)?;
1124+
write!(out, " {{ contains(value: {value})")?;
1125+
}
11081126
ReadExpr::Deletions { input } => {
11091127
render_read_into(input, vars, out)?;
11101128
out.push_str(" { deletions");
@@ -1943,6 +1961,8 @@ fn read_depth(expr: &ReadExpr) -> usize {
19431961
| ReadExpr::History { input }
19441962
| ReadExpr::CombinedHistory { input }
19451963
| ReadExpr::HistoryReverse { input }
1964+
| ReadExpr::HistoryContains { input, .. }
1965+
| ReadExpr::HistoryValueContains { input, .. }
19461966
| ReadExpr::Deletions { input }
19471967
| ReadExpr::Nodes { input }
19481968
| ReadExpr::Neighbours { input }
@@ -3119,6 +3139,8 @@ fn parse_read(expr: &ReadExpr, root: &JsonValue) -> Result<Option<Prop>, ClientE
31193139
// Bool-shaped terminals.
31203140
ReadExpr::HasNode { .. }
31213141
| ReadExpr::HasEdge { .. }
3142+
| ReadExpr::HistoryContains { .. }
3143+
| ReadExpr::HistoryValueContains { .. }
31223144
| ReadExpr::IsActive { .. }
31233145
| ReadExpr::IsValid { .. }
31243146
| ReadExpr::IsDeleted { .. }
@@ -3286,6 +3308,14 @@ fn build_json_path(expr: &ReadExpr) -> Vec<&'static str> {
32863308
go(input, out);
32873309
out.push("reverse");
32883310
}
3311+
ReadExpr::HistoryContains { input, .. } => {
3312+
go(input, out);
3313+
out.push("contains");
3314+
}
3315+
ReadExpr::HistoryValueContains { input, .. } => {
3316+
go(input, out);
3317+
out.push("contains");
3318+
}
32893319
ReadExpr::Deletions { input } => {
32903320
go(input, out);
32913321
out.push("deletions");
@@ -4102,6 +4132,8 @@ fn child_input(expr: &ReadExpr) -> Option<&ReadExpr> {
41024132
| ReadExpr::History { input }
41034133
| ReadExpr::CombinedHistory { input }
41044134
| ReadExpr::HistoryReverse { input }
4135+
| ReadExpr::HistoryContains { input, .. }
4136+
| ReadExpr::HistoryValueContains { input, .. }
41054137
| ReadExpr::Deletions { input }
41064138
| ReadExpr::Nodes { input }
41074139
| ReadExpr::Neighbours { input }

raphtory-graphql/src/client/op.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ pub enum ReadExpr {
127127
/// `History` whose iteration order is flipped. Container-selection like
128128
/// `History`. Server field: `reverse`.
129129
HistoryReverse { input: Arc<ReadExpr> },
130+
/// Terminal on History: whether an entry equal to the given time is
131+
/// present — `bool`. With `event_id`, both fields must match; without it,
132+
/// any entry at the timestamp matches (the two equalities `EventTime`
133+
/// itself defines). Server field: `contains(timestamp:, eventId:)`.
134+
HistoryContains {
135+
input: Arc<ReadExpr>,
136+
timestamp: i64,
137+
event_id: Option<usize>,
138+
},
139+
/// Terminal on a history sub-container (timestamps / event ids /
140+
/// intervals): whether the given value is present — `bool`. Server
141+
/// field: `contains(value:)`.
142+
HistoryValueContains { input: Arc<ReadExpr>, value: i64 },
130143
/// Navigate to the deletion history of an edge. Edge → History.
131144
/// Same shape as `History` but reads the `deletions` server field
132145
/// instead of `history` — deletions are edge-only.

raphtory-graphql/src/client/remote/remote_history.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ impl RemoteHistory {
155155
)
156156
}
157157

158+
/// Terminal: whether an entry equal to the given time is present. With
159+
/// `event_id`, both fields must match; without it, any entry at the
160+
/// timestamp matches. Fires one RPC — the check runs server-side.
161+
pub async fn contains(
162+
&self,
163+
timestamp: i64,
164+
event_id: Option<usize>,
165+
) -> Result<bool, ClientError> {
166+
let op = Op::Read(ReadExpr::HistoryContains {
167+
input: self.expr.clone(),
168+
timestamp,
169+
event_id,
170+
});
171+
expect_bool(self.transport.execute(&op).await?, "contains")
172+
}
173+
158174
/// Sub-container: timestamps view of this history — plain integer
159175
/// timestamps instead of full `EventTime` records. Lazy — no RPC.
160176
pub fn timestamps(&self) -> RemoteHistoryTimestamps {
@@ -228,6 +244,25 @@ pub struct RemoteHistoryTimestamps {
228244
}
229245

230246
impl RemoteHistoryTimestamps {
247+
/// Terminal: number of values in this container. Fires one RPC — the
248+
/// count comes back alone, not the values.
249+
pub async fn count(&self) -> Result<i64, ClientError> {
250+
let op = Op::Read(ReadExpr::Count {
251+
input: self.expr.clone(),
252+
});
253+
expect_i64(self.transport.execute(&op).await?, "count")
254+
}
255+
256+
/// Terminal: whether the given value is present. Fires one RPC — the
257+
/// membership check runs server-side.
258+
pub async fn contains(&self, value: i64) -> Result<bool, ClientError> {
259+
let op = Op::Read(ReadExpr::HistoryValueContains {
260+
input: self.expr.clone(),
261+
value,
262+
});
263+
expect_bool(self.transport.execute(&op).await?, "contains")
264+
}
265+
231266
/// Reversed view of this container. Lazy — no RPC: the reversal wraps the
232267
/// parent history (the server's `reverse` field), so downstream reads and
233268
/// any future optimised iterators compose with it automatically.
@@ -307,6 +342,25 @@ pub struct RemoteHistoryEventIds {
307342
}
308343

309344
impl RemoteHistoryEventIds {
345+
/// Terminal: number of values in this container. Fires one RPC — the
346+
/// count comes back alone, not the values.
347+
pub async fn count(&self) -> Result<i64, ClientError> {
348+
let op = Op::Read(ReadExpr::Count {
349+
input: self.expr.clone(),
350+
});
351+
expect_i64(self.transport.execute(&op).await?, "count")
352+
}
353+
354+
/// Terminal: whether the given value is present. Fires one RPC — the
355+
/// membership check runs server-side.
356+
pub async fn contains(&self, value: i64) -> Result<bool, ClientError> {
357+
let op = Op::Read(ReadExpr::HistoryValueContains {
358+
input: self.expr.clone(),
359+
value,
360+
});
361+
expect_bool(self.transport.execute(&op).await?, "contains")
362+
}
363+
310364
/// Reversed view of this container. Lazy — no RPC: the reversal wraps the
311365
/// parent history (the server's `reverse` field), so downstream reads and
312366
/// any future optimised iterators compose with it automatically.
@@ -399,6 +453,25 @@ fn to_datetimes(timestamps: Vec<i64>) -> Result<Vec<DateTime<Utc>>, ClientError>
399453
}
400454

401455
impl RemoteHistoryDateTimes {
456+
/// Terminal: number of datetimes (one per event). Fires one RPC.
457+
pub async fn count(&self) -> Result<i64, ClientError> {
458+
let op = Op::Read(ReadExpr::Count {
459+
input: self.expr.clone(),
460+
});
461+
expect_i64(self.transport.execute(&op).await?, "count")
462+
}
463+
464+
/// Terminal: whether an event exists at the given epoch-millisecond
465+
/// timestamp — datetimes are derived 1:1 from timestamps, so membership
466+
/// is checked on the timestamp container. Fires one RPC.
467+
pub async fn contains_ms(&self, value: i64) -> Result<bool, ClientError> {
468+
let op = Op::Read(ReadExpr::HistoryValueContains {
469+
input: self.expr.clone(),
470+
value,
471+
});
472+
expect_bool(self.transport.execute(&op).await?, "contains")
473+
}
474+
402475
/// Reversed view of this container. Lazy — no RPC: the reversal wraps the
403476
/// parent history (the server's `reverse` field), so downstream reads and
404477
/// any future optimised iterators compose with it automatically.
@@ -485,6 +558,25 @@ pub struct RemoteIntervals {
485558
}
486559

487560
impl RemoteIntervals {
561+
/// Terminal: number of values in this container. Fires one RPC — the
562+
/// count comes back alone, not the values.
563+
pub async fn count(&self) -> Result<i64, ClientError> {
564+
let op = Op::Read(ReadExpr::Count {
565+
input: self.expr.clone(),
566+
});
567+
expect_i64(self.transport.execute(&op).await?, "count")
568+
}
569+
570+
/// Terminal: whether the given value is present. Fires one RPC — the
571+
/// membership check runs server-side.
572+
pub async fn contains(&self, value: i64) -> Result<bool, ClientError> {
573+
let op = Op::Read(ReadExpr::HistoryValueContains {
574+
input: self.expr.clone(),
575+
value,
576+
});
577+
expect_bool(self.transport.execute(&op).await?, "contains")
578+
}
579+
488580
/// Reversed view of this container. Lazy — no RPC: the reversal wraps the
489581
/// parent history (the server's `reverse` field), so downstream reads and
490582
/// any future optimised iterators compose with it automatically.

raphtory-graphql/src/model/graph/history.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields};
1010
use raphtory::db::api::view::history::{
1111
History, HistoryDateTime, HistoryEventId, HistoryTimestamp, InternalHistoryOps, Intervals,
1212
};
13+
use raphtory_api::core::storage::timeindex::EventTime;
1314
use std::{any::Any, sync::Arc};
1415

1516
/// History of updates for an object in Raphtory.
@@ -151,6 +152,21 @@ impl GqlHistory {
151152
blocking_compute(move || self_clone.history.len() as u64).await
152153
}
153154

155+
/// Whether an entry equal to the given time is present. With `eventId`,
156+
/// an entry must match both the timestamp and the event id; without it,
157+
/// any entry at the timestamp matches.
158+
async fn contains(&self, timestamp: i64, event_id: Option<usize>) -> bool {
159+
let self_clone = self.clone();
160+
blocking_compute(move || match event_id {
161+
Some(event_id) => self_clone
162+
.history
163+
.iter()
164+
.any(|x| x == EventTime::new(timestamp, event_id)),
165+
None => self_clone.history.iter().any(|x| x == timestamp),
166+
})
167+
.await
168+
}
169+
154170
/// Returns a HistoryTimestamp object which accesses timestamps (milliseconds since the Unix epoch)
155171
/// instead of EventTime entries.
156172
async fn timestamps(&self) -> GqlHistoryTimestamp {
@@ -213,6 +229,18 @@ pub struct GqlHistoryTimestamp {
213229

214230
#[ResolvedObjectFields]
215231
impl GqlHistoryTimestamp {
232+
/// Get the number of timestamps (one per entry).
233+
async fn count(&self) -> u64 {
234+
let self_clone = self.clone();
235+
blocking_compute(move || self_clone.history_t.iter().count() as u64).await
236+
}
237+
238+
/// Whether the given value is present among the timestamps.
239+
async fn contains(&self, value: i64) -> bool {
240+
let self_clone = self.clone();
241+
blocking_compute(move || self_clone.history_t.iter().any(|v| v == value)).await
242+
}
243+
216244
/// List all timestamps.
217245
async fn list(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<i64>> {
218246
check_list_allowed(ctx)?;
@@ -500,6 +528,18 @@ pub struct GqlHistoryEventId {
500528

501529
#[ResolvedObjectFields]
502530
impl GqlHistoryEventId {
531+
/// Get the number of event ids (one per entry).
532+
async fn count(&self) -> u64 {
533+
let self_clone = self.clone();
534+
blocking_compute(move || self_clone.history_s.iter().count() as u64).await
535+
}
536+
537+
/// Whether the given value is present among the event ids.
538+
async fn contains(&self, value: u64) -> bool {
539+
let self_clone = self.clone();
540+
blocking_compute(move || self_clone.history_s.iter().any(|v| v as u64 == value)).await
541+
}
542+
503543
/// List event ids.
504544
async fn list(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<u64>> {
505545
check_list_allowed(ctx)?;
@@ -602,6 +642,19 @@ pub struct GqlIntervals {
602642

603643
#[ResolvedObjectFields]
604644
impl GqlIntervals {
645+
/// Get the number of intervals (one per consecutive pair of entries,
646+
/// so one less than the history's count; zero for an empty history).
647+
async fn count(&self) -> u64 {
648+
let self_clone = self.clone();
649+
blocking_compute(move || self_clone.intervals.iter().count() as u64).await
650+
}
651+
652+
/// Whether the given value is present among the intervals.
653+
async fn contains(&self, value: i64) -> bool {
654+
let self_clone = self.clone();
655+
blocking_compute(move || self_clone.intervals.iter().any(|v| v == value)).await
656+
}
657+
605658
/// List time intervals between consecutive timestamps in milliseconds.
606659
async fn list(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<i64>> {
607660
check_list_allowed(ctx)?;

0 commit comments

Comments
 (0)