From bd257cf2ddd214219152fc78a89d546e6af469c6 Mon Sep 17 00:00:00 2001 From: Lionel Sambuc Date: Sat, 25 Jul 2020 13:03:58 +0200 Subject: [PATCH] Use iterators instead of materializing eagerly --- src/database/db_core.rs | 223 ++++++++++++++++++--------------- src/database/mod.rs | 16 ++- src/database/space/position.rs | 3 +- src/database/space/shape.rs | 2 +- src/database/space_db.rs | 66 +++++----- src/database/space_index.rs | 57 +++++---- src/storage/model.rs | 112 ++++++++++++----- 7 files changed, 284 insertions(+), 195 deletions(-) diff --git a/src/database/db_core.rs b/src/database/db_core.rs index 3064ef7..e1b3b56 100644 --- a/src/database/db_core.rs +++ b/src/database/db_core.rs @@ -7,6 +7,8 @@ use super::space::Space; use super::space_db::SpaceDB; use super::space_index::SpaceSetObject; use super::DataBase; +use super::IterObjects; +use super::IterPositions; use super::ResultSet; /// Query Parameters. @@ -220,31 +222,37 @@ impl Core { &self.properties } - fn decode_positions( - list: &mut [(Position, &Properties)], - space: &Space, - db: &DataBase, + fn decode_positions<'b>( + list: IterObjects<'b>, + space: &'b Space, + db: &'b DataBase, output_space: &Option<&str>, - ) -> Result<(), String> { - if let Some(unified_id) = *output_space { + ) -> Result, String> { + let b: IterObjects = if let Some(unified_id) = *output_space { let unified = db.space(unified_id)?; // Rebase the point to the requested output space before decoding. - for (position, _) in list { - *position = unified - .decode(&Space::change_base(&position, space, unified)?)? - .into(); - } + Box::new(list.filter_map(move |(position, properties)| { + match Space::change_base(&position, space, unified) { + Err(_) => None, + Ok(rebased) => match unified.decode(&rebased) { + Err(_) => None, + Ok(decoded) => Some((decoded.into(), properties)), + }, + } + })) } else { // Decode the positions into f64 values, which are defined in their // respective reference space. - for (position, _) in list { - // Simply decode - *position = space.decode(&position)?.into(); - } - } + Box::new(list.filter_map( + move |(position, properties)| match space.decode(&position) { + Err(_) => None, + Ok(decoded) => Some((decoded.into(), properties)), + }, + )) + }; - Ok(()) + Ok(b) } /// Retrieve everything located at specific positions. @@ -262,46 +270,57 @@ impl Core { /// reference space. /// /// [shape]: space/enum.Shape.html - pub fn get_by_positions( - &self, - parameters: &CoreQueryParameters, - positions: &[Position], - space_id: &str, - ) -> ResultSet { + pub fn get_by_positions<'d>( + &'d self, + parameters: &'d CoreQueryParameters, + positions: Vec, + space_id: &'d str, + ) -> ResultSet<'d> { let CoreQueryParameters { db, output_space, .. } = parameters; let mut results = vec![]; - let count = positions.len(); let from = db.space(space_id)?; - // Filter positions based on the view port, if present - let filtered = match parameters.view_port(from) { - None => positions.iter().map(|p| p).collect::>(), - Some(view_port) => positions - .iter() - .filter(|&p| view_port.contains(p)) - .collect::>(), - }; - for s in &self.space_db { let to = db.space(s.name())?; - let mut p = Vec::with_capacity(count); - for position in filtered.as_slice() { - let position: Vec = Space::change_base(position, from, to)?.into(); - p.push(to.encode(&position)?); - } + // Filter positions based on the view port, if present + // FIXME: remove clone() on positions? + let filtered: IterPositions = match parameters.view_port(from) { + None => Box::new(positions.clone().into_iter()), + Some(view_port) => Box::new( + positions + .clone() + .into_iter() + .filter(move |p| view_port.contains(p)), + ), + }; - let mut r = s - .get_by_positions(&p, parameters)? - .into_iter() - .map(|(position, fields)| (position, &self.properties[fields.value()])) - .collect::>(); - Self::decode_positions(r.as_mut_slice(), to, db, output_space)?; + // Rebase the positions into the current space + let p = filtered.filter_map(move |position| { + match Space::change_base(&position, from, to) { + Err(_) => None, + Ok(position) => { + let position: Vec = position.into(); + match to.encode(&position) { + Err(_) => None, + Ok(position) => Some(position), + } + } + } + }); - results.push((s.name(), r)); + // Select the data based on the rebased viewport filter. + let r = s + .get_by_positions(p, parameters)? + .map(move |(position, fields)| (position, &self.properties[fields.value()])); + + results.push(( + s.name(), + Self::decode_positions(Box::new(r), to, db, output_space)?, + )); } Ok(results) @@ -322,12 +341,12 @@ impl Core { /// reference space. /// /// [shape]: space/enum.Shape.html - pub fn get_by_shape( - &self, - parameters: &CoreQueryParameters, - shape: &Shape, - space_id: &str, - ) -> ResultSet { + pub fn get_by_shape<'d>( + &'d self, + parameters: &'d CoreQueryParameters, + shape: Shape, + space_id: &'d str, + ) -> ResultSet<'d> { let CoreQueryParameters { db, output_space, .. } = parameters; @@ -343,14 +362,14 @@ impl Core { // let current_shape = shape.encode(current_space)?; // println!("current shape Encoded: {:?}", current_shape); - let mut r = s - .get_by_shape(¤t_shape, parameters)? - .into_iter() - .map(|(position, fields)| (position, &self.properties[fields.value()])) - .collect::>(); - Self::decode_positions(r.as_mut_slice(), current_space, db, output_space)?; + let r = s + .get_by_shape(current_shape, parameters)? + .map(move |(position, fields)| (position, &self.properties[fields.value()])); - results.push((s.name(), r)); + results.push(( + s.name(), + Self::decode_positions(Box::new(r), current_space, db, output_space)?, + )); } Ok(results) @@ -366,11 +385,11 @@ impl Core { /// * `id`: /// Identifier for which to retrieve is positions. /// - pub fn get_by_id( - &self, - parameters: &CoreQueryParameters, + pub fn get_by_id<'s, S>( + &'s self, + parameters: &'s CoreQueryParameters, id: S, - ) -> Result)>, String> + ) -> Result)>, String> where S: Into, { @@ -391,26 +410,32 @@ impl Core { for s in &self.space_db { let current_space = db.space(s.name())?; - let mut positions = s.get_by_id(offset, parameters)?; + let positions_by_id = s.get_by_id(offset, parameters)?; //Self::decode_positions(r.as_mut_slice(), current_space, db, output_space)?; - if let Some(unified_id) = *output_space { + let positions: IterPositions = if let Some(unified_id) = *output_space { let unified = db.space(unified_id)?; // Rebase the point to the requested output space before decoding. - for position in &mut positions { - *position = unified - .decode(&Space::change_base(position, current_space, unified)?)? - .into(); - } + Box::new(positions_by_id.filter_map(move |position| { + match Space::change_base(&position, current_space, unified) { + Err(_) => None, + Ok(rebased) => match unified.decode(&rebased) { + Err(_) => None, + Ok(decoded) => Some(decoded.into()), + }, + } + })) } else { // Decode the positions into f64 values, which are defined in their // respective reference space. - for position in &mut positions { - // Simply decode - *position = current_space.decode(position)?.into(); - } - } + Box::new(positions_by_id.filter_map(move |position| { + match current_space.decode(&position) { + Err(_) => None, + Ok(decoded) => Some(decoded.into()), + } + })) + }; results.push((s.name(), positions)); } @@ -430,7 +455,11 @@ impl Core { /// * `id`: /// Identifier to use to define the search volume. /// - pub fn get_by_label(&self, parameters: &CoreQueryParameters, id: S) -> ResultSet + pub fn get_by_label<'d, S>( + &'d self, + parameters: &'d CoreQueryParameters, + id: S, + ) -> ResultSet<'d> where S: Into, { @@ -454,7 +483,7 @@ impl Core { let search_volume = self .space_db .iter() - .filter_map(|s| { + .filter_map(move |s| { match db.space(s.name()) { Err(_) => None, Ok(from) => match s.get_by_id(offset, parameters) { @@ -477,40 +506,38 @@ impl Core { }) .flat_map(|v| v); - let search_volume = if let Some(view) = view_port { - search_volume - .filter(|p| view.contains(p)) - .collect::>() - } else { - search_volume.collect::>() - }; - // Select based on the volume, and filter out the label position themselves. for s in &self.space_db { let to = db.space(s.name())?; - let mut p = vec![]; + + let search_volume: IterPositions = if let Some(view) = view_port.clone() { + Box::new(search_volume.clone().filter(move |p| view.contains(p))) + } else { + Box::new(search_volume.clone()) + }; // Convert the search Volume into the target space. - for position in &search_volume { - let position = Space::change_base(position, Space::universe(), to)?; - p.push(position); - } + let p = search_volume.filter_map(move |position| { + match Space::change_base(&position, Space::universe(), to) { + Err(_) => None, + Ok(position) => Some(position), + } + }); - let mut r = s - .get_by_positions(&p, parameters)? - .into_iter() - .filter_map(|(position, fields)| { + let r = s + .get_by_positions(p, parameters)? + .filter_map(move |(position, fields)| { if fields.value() == offset { None } else { Some((position, &self.properties[fields.value()])) } - }) - .collect::>(); + }); - Self::decode_positions(r.as_mut_slice(), to, db, output_space)?; - - results.push((s.name(), r)); + results.push(( + s.name(), + Self::decode_positions(Box::new(r), to, db, output_space)?, + )); } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 109294f..7b15eac 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -14,13 +14,20 @@ pub use db_core::Properties; use space::Position; use space::Space; +/// TODO doc +pub type IterPositions<'i> = Box + 'i>; +/// TODO doc +pub type IterObjects<'i> = Box + 'i>; +/// TODO doc +pub type IterObjectsBySpaces<'i> = Vec<(&'i String, IterObjects<'i>)>; + /// Selected tuples matching a query. /// /// This is either: /// * `Err` with a reason stored as a `String` /// * `Ok`, with a vector of tuples defined as: /// `(Space Name, [(Position, Properties)])` -pub type ResultSet<'r> = Result)>, String>; +pub type ResultSet<'r> = Result, String>; type ReferenceSpaceIndex = ironsea_index_hashmap::Index; type CoreIndex = ironsea_index_hashmap::Index; @@ -128,7 +135,10 @@ impl DataBase { if name == space::Space::universe().name() { Ok(space::Space::universe()) } else { - let r = self.reference_spaces.find(&name.to_string()); + let r = self + .reference_spaces + .find(&name.to_string()) + .collect::>(); Self::check_exactly_one(&r, "spaces", name) } @@ -146,7 +156,7 @@ impl DataBase { /// * `name`: /// The name of the dataset (core) to search for. pub fn core(&self, name: &str) -> Result<&Core, String> { - let r = self.cores.find(&name.to_string()); + let r = self.cores.find(&name.to_string()).collect::>(); Self::check_exactly_one(&r, "cores", name) } diff --git a/src/database/space/position.rs b/src/database/space/position.rs index aeed36e..0e82c8d 100644 --- a/src/database/space/position.rs +++ b/src/database/space/position.rs @@ -60,7 +60,8 @@ impl Position { /// Compute `||self||`. pub fn norm(&self) -> f64 { if let Position::Position1(coordinates) = self { - // the square root of a single number to the square is its positive value, so ensure it is. + // the square root of a single number to the square is its + // positive value, so ensure it is. coordinates.f64().abs() } else { let point: Vec<&Coordinate> = self.into(); diff --git a/src/database/space/shape.rs b/src/database/space/shape.rs index 8e658d3..76ea088 100644 --- a/src/database/space/shape.rs +++ b/src/database/space/shape.rs @@ -39,7 +39,7 @@ impl Shape { //FIXME: Is the length properly dealt with? How do we process this for space conversions? let mut r = Vec::with_capacity(center.dimensions()); for _ in 0..center.dimensions() { - r.push(radius.clone()); + r.push(*radius); } let r = r.into(); let r = from.absolute_position(&r)?; diff --git a/src/database/space_db.rs b/src/database/space_db.rs index a1bbb6a..c4f4750 100644 --- a/src/database/space_db.rs +++ b/src/database/space_db.rs @@ -15,6 +15,7 @@ use super::space_index::SpaceIndex; use super::space_index::SpaceSetIndex; use super::space_index::SpaceSetObject; use super::CoreQueryParameters; +use super::IterPositions; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SpaceDB { @@ -59,8 +60,7 @@ impl SpaceDB { } // Apply fixed scales - let mut count = 0; - for power in &powers { + for (count, power) in powers.iter().enumerate() { space_objects = space_objects .into_iter() .map(|mut o| { @@ -79,8 +79,7 @@ impl SpaceDB { .collect(); // Make sure we do not shift more position than available - let shift = if count >= 31 { 31 } else { count }; - count += 1; + let shift = if count >= 31 { 31 } else { count as u32 }; indices.push(( SpaceSetIndex::new(space_objects.iter(), DIMENSIONS, CELL_BITS), vec![power.0, power.0, power.0], @@ -279,11 +278,11 @@ impl SpaceDB { // Search by Id, a.k.a values // The results are in encoded space coordinates. - pub fn get_by_id( - &self, + pub fn get_by_id<'s>( + &'s self, id: usize, parameters: &CoreQueryParameters, - ) -> Result, String> { + ) -> Result, String> { // Is that ID referenced in the current space? let index = self.resolution(parameters); @@ -292,15 +291,20 @@ impl SpaceDB { let view_port = parameters.view_port(space); // Select the objects - let objects = self.resolutions[index].find_by_value(&SpaceFields::new(self.name(), id)); + // FIXME: How to return an iterator instead of instantiating all + // the points here? Needed because of &SpaceFields. + let objects = self.resolutions[index] + .find_by_value(&SpaceFields::new(self.name(), id)) + .collect::>(); - let results = if let Some(view_port) = view_port { - objects - .into_iter() - .filter(|position| view_port.contains(position)) - .collect::>() + let results: IterPositions<'s> = if let Some(view_port) = view_port { + Box::new( + objects + .into_iter() + .filter(move |position| view_port.contains(position)), + ) } else { - objects + Box::new(objects.into_iter()) }; Ok(results) @@ -308,11 +312,11 @@ impl SpaceDB { // Search by positions defining a volume. // The position is expressed in encoded space coordinates, and results are in encoded space coordinates. - pub fn get_by_positions( - &self, - positions: &[Position], + pub fn get_by_positions<'s>( + &'s self, + positions: impl Iterator + 's, parameters: &CoreQueryParameters, - ) -> Result, String> { + ) -> Result + 's>, String> { let index = self.resolution(parameters); // FIXME: Should I do it here, or add the assumption this is a clean list? @@ -321,17 +325,13 @@ impl SpaceDB { //let view_port = parameters.view_port(space); // Select the objects - let results = positions - .iter() - .flat_map(|position| { - self.resolutions[index] - .find(position) - .into_iter() - .map(move |fields| (position.clone(), fields)) - }) - .collect(); + let results = positions.flat_map(move |position| { + self.resolutions[index] + .find(&position) + .map(move |fields| (position.clone(), fields)) + }); - Ok(results) + Ok(Box::new(results)) } // Search by Shape defining a volume: @@ -340,11 +340,11 @@ impl SpaceDB { // * Point (Specific position) // The Shape is expressed in encoded space coordinates, and results are in encoded space coordinates. - pub fn get_by_shape( - &self, - shape: &Shape, + pub fn get_by_shape<'s>( + &'s self, + shape: Shape, parameters: &CoreQueryParameters, - ) -> Result, String> { + ) -> Result + 's>, String> { let index = self.resolution(parameters); // Convert the view port to the encoded space coordinates @@ -352,7 +352,7 @@ impl SpaceDB { let view_port = parameters.view_port(space); // Select the objects - let results = self.resolutions[index].find_by_shape(&shape, &view_port)?; + let results = self.resolutions[index].find_by_shape(shape, &view_port)?; Ok(results) } diff --git a/src/database/space_index.rs b/src/database/space_index.rs index 5fdcf6f..3b6b1f4 100644 --- a/src/database/space_index.rs +++ b/src/database/space_index.rs @@ -7,6 +7,7 @@ use serde::Serialize; use super::space::Coordinate; use super::space::Position; use super::space::Shape; +use super::IterPositions; #[derive(Clone, Debug, Hash)] pub struct SpaceSetObject { @@ -125,59 +126,63 @@ impl SpaceIndex { } // Inputs and Results are expressed in encoded space coordinates. - pub fn find(&self, key: &Position) -> Vec<&SpaceFields> { + pub fn find<'s>(&'s self, key: &Position) -> Box + 's> { self.index.find(key) } // Inputs and Results are expressed in encoded space coordinates. - fn find_range(&self, start: &Position, end: &Position) -> Vec<(Position, &SpaceFields)> { + fn find_range<'s>( + &'s self, + start: &Position, + end: &Position, + ) -> Box + 's> { self.index.find_range(start, end) } // Inputs and Results are expressed in encoded space coordinates. - pub fn find_by_value(&self, id: &SpaceFields) -> Vec { + pub fn find_by_value<'s>(&'s self, id: &'s SpaceFields) -> IterPositions<'s> { self.index.find_by_value(id) } // Inputs and Results are also in encoded space coordinates. - pub fn find_by_shape( - &self, - shape: &Shape, + pub fn find_by_shape<'s>( + &'s self, + shape: Shape, view_port: &Option, - ) -> Result, String> { + ) -> Result + 's>, String> { match shape { Shape::Point(position) => { if let Some(mbb) = view_port { - if !mbb.contains(position) { + if !mbb.contains(&position) { return Err(format!( "View port '{:?}' does not contain '{:?}'", mbb, position )); } } - Ok(self - .find(position) - .into_iter() - .map(|fields| (position.clone(), fields)) - .collect()) + Ok(Box::new( + self.find(&position) + .map(move |fields| (position.clone(), fields)), + )) } Shape::BoundingBox(bl, bh) => { if let Some(mbb) = view_port { match mbb { Shape::BoundingBox(vl, vh) => { // Compute the intersection of the two boxes. - let lower = bl.max(vl); - let higher = bh.min(vh); + let lower = (&bl).max(vl); + let higher = (&bh).min(vh); if higher < lower { Err(format!( "View port '{:?}' does not intersect '{:?}'", - mbb, shape + mbb, + Shape::BoundingBox(bl.clone(), bh.clone()) )) } else { trace!( "mbb {:?} shape {:?} lower {:?} higher {:?}", mbb, - shape, + Shape::BoundingBox(bl.clone(), bh.clone()), lower, higher ); @@ -187,11 +192,11 @@ impl SpaceIndex { _ => Err(format!("Invalid view port shape '{:?}'", mbb)), } } else { - Ok(self.find_range(bl, bh)) + Ok(self.find_range(&bl, &bh)) } } Shape::HyperSphere(center, radius) => { - let (bl, bh) = &shape.get_mbb(); + let (bl, bh) = Shape::HyperSphere(center.clone(), radius).get_mbb(); let lower; let higher; @@ -199,14 +204,14 @@ impl SpaceIndex { match mbb { Shape::BoundingBox(vl, vh) => { // Compute the intersection of the two boxes. - lower = bl.max(vl); - higher = bh.min(vh); + lower = (&bl).max(vl); + higher = (&bh).min(vh); } _ => return Err(format!("Invalid view port shape '{:?}'", mbb)), } } else { - lower = bl; - higher = bh; + lower = &bl; + higher = &bh; } // Filter out results using using a range query over the MBB, @@ -214,11 +219,9 @@ impl SpaceIndex { // a sphere. let results = self .find_range(&lower, &higher) - .into_iter() - .filter(|(position, _)| (position - center).norm() <= radius.f64()) - .collect(); + .filter(move |(position, _)| (position - ¢er).norm() <= radius.f64()); - Ok(results) + Ok(Box::new(results)) } } } diff --git a/src/storage/model.rs b/src/storage/model.rs index 698a257..d35bd2b 100644 --- a/src/storage/model.rs +++ b/src/storage/model.rs @@ -68,9 +68,8 @@ pub mod v1 { use serde::Deserialize; use serde::Serialize; - use crate::database; - use database::space; - + use super::database; + use super::space; use super::Point; use super::Properties; @@ -87,7 +86,8 @@ pub mod v1 { /// Define a Shape, within a specific reference space. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Shape { - /// Type of the shape, which is used to interpret the list of `vertices`. + /// Type of the shape, which is used to interpret the list of + /// `vertices`. #[serde(rename = "type")] pub type_name: String, @@ -99,8 +99,8 @@ pub mod v1 { pub vertices: Vec, } - /// Convert a list of properties grouped by space id, then positions to a - /// list of Spatial Objects for the rest API v1. + /// Convert a list of properties grouped by space id, then positions + /// to a list of Spatial Objects for the rest API v1. /// /// # Parameters /// @@ -109,7 +109,8 @@ pub mod v1 { pub fn to_spatial_objects( list: Vec<(&String, Vec<(space::Position, &database::Properties)>)>, ) -> Vec { - // Filter per Properties, in order to regroup by it, then build a single SpatialObject per Properties. + // Filter per Properties, in order to regroup by it, then build + // a single SpatialObject per Properties. let mut hashmap = HashMap::new(); for (space, v) in list { for (position, properties) in v { @@ -150,9 +151,8 @@ pub mod v2 { use serde::Deserialize; use serde::Serialize; - use crate::database; - use database::space; - + use super::database; + use super::space; use super::Point; use super::Properties; @@ -208,46 +208,91 @@ pub mod v2 { HyperSpheres(Vec<(Point, f64)>), } - /// Convert a list of properties grouped by space id, then positions to a - /// list of Spatial Objects for the rest API v2. + /// Convert a list of space id grouped by properties, then positions + /// to a list of Spatial Objects for the rest API v2. + /// + /// # Parameters + /// + /// * `list`: + /// A list of (`&Properties`, [ ( **Space Id**, [ *Spatial position* ] ) ]) tuples. + #[allow(clippy::type_complexity)] + // Type alias cannot be used as Traits, so we can't use the alias to add the lifetime specifications. + pub fn from_spaces_by_properties<'o>( + objects: Box< + (dyn Iterator< + Item = ( + &'o database::Properties, + Vec<(&'o String, Box + 'o>)>, + ), + > + 'o), + >, + ) -> impl Iterator + 'o { + objects.map(|(property, positions_by_spaces)| { + let volumes = positions_by_spaces + .into_iter() + .map(|(space, positions)| { + // We are not using vec![] as we now beforehand we + // will have only one element in the vector, so we + // optimise for space by allocating it as such. + let mut shapes = Vec::with_capacity(1); + + shapes.push(Shape::Points( + positions + .map(|position| position.into()) + .collect::>(), + )); + + Volume { + space: space.clone(), + shapes, + } + }) + .collect(); + + SpatialObject { + properties: (&property).into(), + volumes, + } + }) + } + + /// Convert a list of properties grouped by space id, then positions + /// to a list of Spatial Objects for the rest API v2. /// /// # Parameters /// /// * `list`: /// A list of (**Space Id**, [ ( *Spatial position*, `&Properties` ) ]) tuples. - pub fn to_spatial_objects( - list: Vec<(&String, Vec<(space::Position, &database::Properties)>)>, - ) -> Vec { - // Filter per Properties, in order to regroup by it, then build a single SpatialObject per Properties. + pub fn from_properties_by_spaces<'o>( + objects: database::IterObjectsBySpaces<'o>, + ) -> impl Iterator + 'o { + // Filter per Properties, in order to regroup by it, then build + // a single SpatialObject per Properties. let mut hashmap = HashMap::new(); - for (space, v) in list { + for (space, v) in objects { for (position, properties) in v { hashmap .entry(properties) .or_insert_with(HashMap::new) .entry(space) .or_insert_with(Vec::new) - .push(position.into()); + .push(position); } } - let mut results = vec![]; - for (properties, v) in hashmap.iter_mut() { - let volumes = v - .drain() - .map(|(space, positions)| Volume { - space: space.clone(), - shapes: vec![Shape::Points(positions)], + let results = Box::new(hashmap.into_iter().map(|(property, hm)| { + let positions = hm + .into_iter() + .map(|(space, positions)| { + let positions: database::IterPositions = Box::new(positions.into_iter()); + (space, positions) }) - .collect(); + .collect::>(); - results.push(SpatialObject { - properties: properties.into(), - volumes, - }); - } + (property, positions) + })); - results + from_spaces_by_properties(results) } } @@ -410,6 +455,9 @@ pub fn build_index( } properties.append(&mut properties_hm.drain().map(|(_, v)| v).collect::>()); + + // We we use sort_by_key, we get borrow checker errors. + #[allow(clippy::unnecessary_sort_by)] properties.sort_unstable_by(|a, b| a.id().cmp(b.id())); space_set_objects.iter_mut().for_each(|object| {