Skip to content

Distinct History Object for nodes and edges #2075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2dcdbdb
Implementing HistoryObject for node and edge objects. Currently, the …
arienandalibi May 3, 2025
95ad6e5
Updated HistoryObject to now be called HistoryImplemented, and Histor…
arienandalibi May 6, 2025
0a538a1
Updated HistoryImplemented to have merge() and compose() functions. T…
arienandalibi May 7, 2025
9e0ca20
Updated history object. Began implementing the pyo3 interface. The py…
arienandalibi May 8, 2025
79c83ce
Implemented preliminary PyO3 file which currently compiles. We can ge…
arienandalibi May 9, 2025
4ea13e9
Added PartialEq and Eq to History Objects, which compare their iterat…
arienandalibi May 12, 2025
84d9cd4
History object is now fully accessible in python. Merge and composite…
arienandalibi May 12, 2025
88ba456
Added compose_from_histories() function.
arienandalibi May 14, 2025
08e769d
CompositeHistory object now uses Arc instead of Box as its pointers. …
arienandalibi May 15, 2025
60f99b6
Nodes and Edges return the history object when calling history on the…
arienandalibi May 19, 2025
4b92da4
Implemented tests for History object in graphql using python. Tested …
arienandalibi May 21, 2025
ad5ebd3
Renamed TemporalProp to TemporalProperty
arienandalibi May 22, 2025
5eb9ce7
Merge remote-tracking branch 'origin/master' into feature/history-object
arienandalibi May 22, 2025
2b824c1
Synced with master and ran rustfmt
arienandalibi May 22, 2025
60e37b4
Made TimeIndexEntry available in Python so that they can be created a…
arienandalibi May 22, 2025
9cc58b1
Ran rustfmt
arienandalibi May 22, 2025
e5e47e6
Trying to change NodeOp for History to return the history object rath…
arienandalibi May 23, 2025
5e6c6e9
Renamed the history operation from History to HistoryOp, the history …
arienandalibi May 27, 2025
56ec538
Updated the rest of the code to work with new history object being re…
arienandalibi May 27, 2025
0c0614e
Implemented InternalHistoryOps for TemporalProperties
arienandalibi May 27, 2025
fc54718
Changed InternalHistoryOps implementation from TemporalProperties to …
arienandalibi May 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pometry-storage-private
Submodule pometry-storage-private deleted from 5e8118
319 changes: 319 additions & 0 deletions python/tests/test_base_install/test_graphql/test_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import json
from utils import run_graphql_test
from raphtory import Graph
from raphtory.graphql import *

work_dir = "/tmp/harry_potter_graph/"
PORT = 1737


def write_graph(tmp_work_dir: str = work_dir):
graph = create_graph()
server = graphql.GraphServer(tmp_work_dir)
client = server.start().get_client()
client.send_graph(path="harry_potter", graph=graph)


def print_query_output(query: str, graph: Graph, tmp_work_dir: str = work_dir):
with graphql.GraphServer(tmp_work_dir).start(PORT) as server:
client = server.get_client()
client.send_graph(path="harry_potter", graph=graph, overwrite=True)
response = client.query(query)

# Convert response to a dictionary if needed and compare
response_dict = json.loads(response) if isinstance(response, str) else response
print(response_dict)


def run_graphql_test(query: str, expected_output: dict, graph: Graph, tmp_work_dir: str = work_dir):
with GraphServer(tmp_work_dir).start(PORT) as server:
client = server.get_client()
client.send_graph(path="harry_potter", graph=graph, overwrite=True)
response = client.query(query)

# Convert response to a dictionary if needed and compare
response_dict = json.loads(response) if isinstance(response, str) else response
assert response_dict == expected_output


def create_graph() -> Graph:
graph = Graph()

# Add nodes with timestamps and properties
graph.add_node(100, "Dumbledore")
graph.add_node(200, "Dumbledore", properties={"Age": 50})
graph.add_node(300, "Dumbledore", properties={"Age": 51})

graph.add_node(150, "Harry")
graph.add_node(250, "Harry", properties={"Age": 20})
graph.add_node(350, "Harry", properties={"Age": 21})

# Add edges with timestamps and layers
graph.add_edge(150, "Dumbledore", "Harry", layer="communication")
graph.add_edge(200, "Dumbledore", "Harry", properties={"weight": 0.5}, layer="friendship")
graph.add_edge(300, "Dumbledore", "Harry", properties={"weight": 0.7}, layer="communication")
graph.add_edge(350, "Dumbledore", "Harry", properties={"weight": 0.9}, layer="friendship")
return graph


def test_history_node():
# FIXME: When viewing graph, "Field "history" of type "History" must have a selection of subfields"
graph = create_graph()
query = """
{
graph(path:"harry_potter"){
node(name: "Dumbledore"){
history{
timestamps
}
}
}
}
"""
expected_output = {'graph': {'node': {'history': {'timestamps': [100, 150, 200, 200, 300, 300, 350]}}}}
run_graphql_test(query, expected_output, graph)


def test_history_edge():
graph = create_graph()
query = """
{
graph(path: "harry_potter") {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
"""
expected_output = {'graph': {'edge': {'history': {'timestamps': [150, 200, 300, 350]}}}}
run_graphql_test(query, expected_output, graph)


def test_history_window_node():
graph = create_graph()
# window(0, 150)
query = """
{
graph(path: "harry_potter") {
window(start: 0, end: 150) {
node(name: "Dumbledore") {
history {
timestamps
}
}
}
}
}
"""
expected_output_1 = {'graph': {'window': {'node': {'history': {'timestamps': [100]}}}}}
run_graphql_test(query, expected_output_1, graph)

# window(150, 300)
query = """
{
graph(path: "harry_potter") {
window(start: 150, end: 300) {
node(name: "Dumbledore") {
history {
timestamps
}
}
}
}
}
"""
expected_output_2 = {'graph': {'window': {'node': {'history': {'timestamps': [150, 200, 200]}}}}}
run_graphql_test(query, expected_output_2, graph)

# window(300, 450)
query = """
{
graph(path: "harry_potter") {
window(start: 300, end: 450) {
node(name: "Dumbledore") {
history {
timestamps
}
}
}
}
}
"""
expected_output_3 = {'graph': {'window': {'node': {'history': {'timestamps': [300, 300, 350]}}}}}
run_graphql_test(query, expected_output_3, graph)


def test_history_window_edge():
graph = create_graph()
# window(0, 150)
query = """
{
graph(path: "harry_potter") {
window(start: 0, end: 150) {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_1 = {'graph': {'window': {'edge': None}}}
run_graphql_test(query, expected_output_1, graph)

# window(150, 300)
query = """
{
graph(path: "harry_potter") {
window(start: 150, end: 300) {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_2 = {'graph': {'window': {'edge': {'history': {'timestamps': [150, 200]}}}}}
run_graphql_test(query, expected_output_2, graph)

# window(300, 450)
query = """
{
graph(path: "harry_potter") {
window(start: 300, end: 450) {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_3 = {'graph': {'window': {'edge': {'history': {'timestamps': [300, 350]}}}}}
run_graphql_test(query, expected_output_3, graph)

def test_history_layer_node():
graph = create_graph()
query = """
{
graph(path: "harry_potter") {
layer(name: "friendship") {
node(name: "Dumbledore") {
history {
timestamps
}
}
}
}
}
"""
# FIXME: Currently fails because the layer doesn't affect node history
expected_output = {'graph': {'layer': {'node': {'history': {'timestamps': [100, 200, 200, 300, 350]}}}}}
# run_graphql_test(query, expected_output, graph)

def test_history_layer_edge():
graph = create_graph()
query = """
{
graph(path: "harry_potter") {
layer(name: "communication") {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output = {'graph': {'layer': {'edge': {'history': {'timestamps': [150, 300]}}}}}
run_graphql_test(query, expected_output, graph)


def test_history_prop_filter_edge():
graph = create_graph()
# the edge only has one value associated to "weight"
# even tho we updated the edge with multiple different weights at different timestamps, there is only 0.9 (the latest)
query_1 = """
{
graph(path: "harry_potter") {
edgeFilter(
property: "weight"
condition: {operator: EQUAL, value: {f64: 0.9}}) {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_1 = {'graph': {'edgeFilter': {'edge': {'history': {'timestamps': [150, 200, 300, 350]}}}}}
run_graphql_test(query_1, expected_output_1, graph)

# other weights should have no hits because the weight is updated, not appended
query_2 = """
{
graph(path: "harry_potter") {
edgeFilter(
property: "weight"
condition: {operator: EQUAL, value: {f64: 0.7}}) {
edge(src: "Dumbledore", dst: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_2 = {'graph': {'edgeFilter': {'edge': None}}}
run_graphql_test(query_2, expected_output_2, graph)

def test_history_prop_filter_node():
graph = create_graph()
# the edge only has one value associated to "weight"
# even tho we updated the edge with multiple different weights at different timestamps, there is only 0.9 (the latest)
query_1 = """
{
graph(path: "harry_potter") {
nodeFilter(
property: "Age"
condition: {operator: LESS_THAN, value: {i64: 30}}) {
node(name: "Dumbledore") {
history {
timestamps
}
}
}
}
}
"""
expected_output_1 = {'graph': {'nodeFilter': {'node': None}}}
run_graphql_test(query_1, expected_output_1, graph)

# other weights should have no hits because the weight is updated, not appended
query_2 = """
{
graph(path: "harry_potter") {
nodeFilter(
property: "Age"
condition: {operator: GREATER_THAN, value: {i64: 30}}) {
node(name: "Harry") {
history {
timestamps
}
}
}
}
}
"""
expected_output_2 = {'graph': {'nodeFilter': {'node': None}}}
run_graphql_test(query_2, expected_output_2, graph)
1 change: 1 addition & 0 deletions raphtory-api/src/python/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod arcstr;
mod direction;
mod gid;
pub mod timeindex;
50 changes: 50 additions & 0 deletions raphtory-api/src/python/timeindex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::core::storage::timeindex::{AsTime, TimeIndexEntry};
use chrono::{DateTime, Utc};
use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, Python};

impl<'py> IntoPyObject<'py> for TimeIndexEntry {
type Target = PyRaphtoryTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
PyRaphtoryTime::from(self).into_pyobject(py)
}
}

#[pyclass(name = "RaphtoryTime", module = "raphtory", frozen, eq, ord)]
#[derive(Debug, Clone, PartialEq, Ord, PartialOrd, Eq)]
pub struct PyRaphtoryTime {
time: TimeIndexEntry,
}

#[pymethods]
impl PyRaphtoryTime {
/// Get the datetime representation of the time
pub fn dt(&self) -> Option<DateTime<Utc>> {
self.time.dt()
}

/// Get the epoch timestamp of the time
pub fn epoch(&self) -> i64 {
self.time.t()
}

pub fn __repr__(&self) -> String {
format!("TimeIndexEntry[{}, {}]", self.time.0, self.time.1)
}

// TODO: Might wanna remove this later
#[staticmethod]
pub fn new(t: i64, s: usize) -> Self {
Self {
time: TimeIndexEntry::new(t, s),
}
}
}

impl From<TimeIndexEntry> for PyRaphtoryTime {
fn from(time: TimeIndexEntry) -> Self {
Self { time }
}
}
6 changes: 4 additions & 2 deletions raphtory-graphql/src/model/graph/edge.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::model::graph::history::GqlHistory;
use crate::model::graph::{
edges::GqlEdges, filtering::EdgeViewCollection, node::Node, property::GqlProperties,
};
use dynamic_graphql::{ResolvedObject, ResolvedObjectFields};
use raphtory::db::api::view::history::History;
use raphtory::{
core::utils::errors::GraphError,
db::{
Expand Down Expand Up @@ -245,8 +247,8 @@ impl Edge {
GqlEdges::new(self.ee.explode_layers())
}

async fn history(&self) -> Vec<i64> {
self.ee.history()
async fn history(&self) -> GqlHistory {
History::new(self.ee.clone()).into()
}

async fn deletions(&self) -> Vec<i64> {
Expand Down
Loading
Loading