Skip to content

Make VID element internal to the Entity #5857

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

Merged
merged 8 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 1 addition & 12 deletions graph/src/components/store/entity_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,7 @@ impl EntityCache {
// always creates it in a new style.
debug_assert!(match scope {
GetScope::Store => {
// Release build will never call this function and hence it's OK
// when that implementation is not correct.
fn remove_vid(entity: Option<Arc<Entity>>) -> Option<Entity> {
entity.map(|e| {
#[allow(unused_mut)]
let mut entity = (*e).clone();
#[cfg(debug_assertions)]
entity.remove("vid");
entity
})
}
remove_vid(entity.clone()) == remove_vid(self.store.get(key).unwrap().map(Arc::new))
entity == self.store.get(key).unwrap().map(Arc::new)
}
GetScope::InBlock => true,
});
Expand Down
88 changes: 74 additions & 14 deletions graph/src/data/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,16 +740,17 @@ lazy_static! {
}

/// An entity is represented as a map of attribute names to values.
#[derive(Clone, CacheWeight, PartialEq, Eq, Serialize)]
#[derive(Clone, CacheWeight, Eq, Serialize)]
pub struct Entity(Object<Value>);

impl<'a> IntoIterator for &'a Entity {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also needs to be changed so that entity.iter() and entity.into_iter() do not show the vid, something like

impl<'a> IntoIterator for &'a Entity {
    type Item = (Word, Value);

    type IntoIter = intern::ObjectOwningIter<Value>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.clone().into_iter().filter(|(k,_)| k != VID_FIELD)
    }
}

Copy link
Collaborator

@lutter lutter Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also add unit tests at teh end of the file that show that the various methods behave well when there is a vid (e.g., that iteration and sorted do not include the vid)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best I could do is use retain(). Can we postpone the UT for later moment?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that the original already had that really surprising and unnecessary clone in it. After playing around a little and consulting this, I think this would work and gets rid of the clone, too:

impl<'a> IntoIterator for &'a Entity {
    type Item = (&'a str, &'a Value);

    type IntoIter =
        std::iter::Filter<intern::ObjectIter<'a, Value>, fn(&(&'a str, &'a Value)) -> bool>;

    fn into_iter(self) -> Self::IntoIter {
        (&self.0).into_iter().filter(|(k, _)| *k != VID_FIELD)
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful! That removes two copies - the original clone and my retain! BTW added the test.

type Item = (Word, Value);
type Item = (&'a str, &'a Value);

type IntoIter = intern::ObjectOwningIter<Value>;
type IntoIter =
std::iter::Filter<intern::ObjectIter<'a, Value>, fn(&(&'a str, &'a Value)) -> bool>;

fn into_iter(self) -> Self::IntoIter {
self.0.clone().into_iter()
(&self.0).into_iter().filter(|(k, _)| *k != VID_FIELD)
}
}

Expand Down Expand Up @@ -874,10 +875,18 @@ impl Entity {
}

pub fn get(&self, key: &str) -> Option<&Value> {
// VID field is private and not visible outside
if key == VID_FIELD {
return None;
}
self.0.get(key)
}

pub fn contains_key(&self, key: &str) -> bool {
// VID field is private and not visible outside
if key == VID_FIELD {
return false;
}
self.0.contains_key(key)
}

Expand Down Expand Up @@ -919,7 +928,8 @@ impl Entity {
/// Return the VID of this entity and if its missing or of a type different than
/// i64 it panics.
pub fn vid(&self) -> i64 {
self.get(VID_FIELD)
self.0
.get(VID_FIELD)
.expect("the vid must be set")
.as_int8()
.expect("the vid must be set to a valid value")
Expand All @@ -930,15 +940,6 @@ impl Entity {
self.0.insert(VID_FIELD, value.into())
}

/// Sets the VID if it's not already set. Should be used only for tests.
#[cfg(debug_assertions)]
pub fn set_vid_if_empty(&mut self) {
let vid = self.get(VID_FIELD);
if vid.is_none() {
let _ = self.set_vid(100).expect("the vid should be set");
}
}

/// Merges an entity update `update` into this entity.
///
/// If a key exists in both entities, the value from `update` is chosen.
Expand Down Expand Up @@ -1062,6 +1063,13 @@ impl Entity {
}
}

/// Checks equality of two entities while ignoring the VID fields
impl PartialEq for Entity {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_key(&other.0, VID_FIELD)
}
}

/// Convenience methods to modify individual attributes for tests.
/// Production code should not use/need this.
#[cfg(debug_assertions)]
Expand All @@ -1081,6 +1089,14 @@ impl Entity {
) -> Result<Option<Value>, InternError> {
self.0.insert(name, value.into())
}

/// Sets the VID if it's not already set. Should be used only for tests.
pub fn set_vid_if_empty(&mut self) {
let vid = self.0.get(VID_FIELD);
if vid.is_none() {
let _ = self.set_vid(100).expect("the vid should be set");
}
}
}

impl<'a> From<&'a Entity> for Cow<'a, Entity> {
Expand Down Expand Up @@ -1272,3 +1288,47 @@ fn fmt_debug() {
let bi = Value::BigInt(scalar::BigInt::from(-17i32));
assert_eq!("BigInt(-17)", format!("{:?}", bi));
}

#[test]
fn entity_hidden_vid() {
use crate::schema::InputSchema;
let subgraph_id = "oneInterfaceOneEntity";
let document = "type Thing @entity {id: ID!, name: String!}";
let schema = InputSchema::raw(document, subgraph_id);

let entity = entity! { schema => id: "1", name: "test", vid: 3i64 };
let debug_str = format!("{:?}", entity);
let entity_str = "Entity { id: String(\"1\"), name: String(\"test\"), vid: Int8(3) }";
assert_eq!(debug_str, entity_str);

// get returns nothing...
assert_eq!(entity.get(VID_FIELD), None);
assert_eq!(entity.contains_key(VID_FIELD), false);
// ...while vid is present
assert_eq!(entity.vid(), 3i64);

// into_iter() misses it too
let mut it = entity.into_iter();
assert_eq!(Some(("id", &Value::String("1".to_string()))), it.next());
assert_eq!(
Some(("name", &Value::String("test".to_string()))),
it.next()
);
assert_eq!(None, it.next());

let mut entity2 = entity! { schema => id: "1", name: "test", vid: 5i64 };
assert_eq!(entity2.vid(), 5i64);
// equal with different vid
assert_eq!(entity, entity2);

entity2.remove(VID_FIELD);
// equal if one has no vid
assert_eq!(entity, entity2);
let debug_str2 = format!("{:?}", entity2);
let entity_str2 = "Entity { id: String(\"1\"), name: String(\"test\") }";
assert_eq!(debug_str2, entity_str2);

// set again
_ = entity2.set_vid(7i64);
assert_eq!(entity2.vid(), 7i64);
}
39 changes: 39 additions & 0 deletions graph/src/util/intern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,45 @@ impl<V> Object<V> {
}
}

impl<V: PartialEq> Object<V> {
fn len_ignore_atom(&self, atom: &Atom) -> usize {
// Because of tombstones and the ignored atom, we can't just return `self.entries.len()`.
self.entries
.iter()
.filter(|entry| entry.key != TOMBSTONE_KEY && entry.key != *atom)
.count()
}

/// Check for equality while ignoring one particular element
pub fn eq_ignore_key(&self, other: &Self, ignore_key: &str) -> bool {
let ignore = self.pool.lookup(ignore_key);
let len1 = if let Some(to_ignore) = ignore {
self.len_ignore_atom(&to_ignore)
} else {
self.len()
};
let len2 = if let Some(to_ignore) = other.pool.lookup(ignore_key) {
other.len_ignore_atom(&to_ignore)
} else {
other.len()
};
if len1 != len2 {
return false;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very minor, but since you need to traverse the list of key/value pairs to determine the length, there's not much value in trying to short-circuit the comparison based on length since you already need to load everything into cache. I would just leave the length-based optimization out and go straight to comparing based on values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought the size comparison avoids false positive when elements in self are subset of the ones ni other, so I'll keep it.


if self.same_pool(other) {
self.entries
.iter()
.filter(|e| e.key != TOMBSTONE_KEY && ignore.map_or(true, |ig| e.key != ig))
.all(|Entry { key, value }| other.get_by_atom(key).map_or(false, |o| o == value))
} else {
self.iter()
.filter(|(key, _)| *key != ignore_key)
.all(|(key, value)| other.get(key).map_or(false, |o| o == value))
}
}
}

impl<V: NullValue> Object<V> {
/// Remove `key` from the object and return the value that was
/// associated with the `key`. The entry is actually not removed for
Expand Down
Loading