Experiments in applying Entity-Component-System patterns to durable data storage APIs.
at main 13 kB view raw
1use rusqlite::{OptionalExtension, params}; 2use tracing::{debug, trace}; 3 4use crate::{ 5 Component, CreatedAt, DynComponent, Ecs, EntityId, Error, LastUpdated, 6 component::Bundle, 7 query::{self}, 8}; 9 10#[derive(Debug, Copy, Clone)] 11pub struct WithoutEntityId; 12#[derive(Debug, Copy, Clone)] 13pub struct WithEntityId(EntityId); 14 15pub type Entity<'a> = GenericEntity<'a, WithEntityId>; 16pub type NewEntity<'a> = GenericEntity<'a, WithoutEntityId>; 17 18#[derive(Copy, Clone)] 19pub struct GenericEntity<'a, S>(&'a Ecs, S); 20 21impl<'a, T> GenericEntity<'a, T> { 22 pub(crate) fn without_id(ecs: &'a Ecs) -> NewEntity<'a> { 23 GenericEntity(ecs, WithoutEntityId) 24 } 25 26 pub(crate) fn with_id(ecs: &'a Ecs, eid: EntityId) -> Entity<'a> { 27 GenericEntity(ecs, WithEntityId(eid)) 28 } 29 30 pub fn db(&'a self) -> &'a Ecs { 31 self.0 32 } 33} 34 35impl<'a> Entity<'a> { 36 pub fn id(&self) -> EntityId { 37 (self.1).0 38 } 39 40 pub fn exists(&self) -> bool { 41 self.try_exists().expect("Entity::try_exists") 42 } 43 44 #[tracing::instrument(name = "exists", level = "debug")] 45 pub fn try_exists(&self) -> Result<bool, Error> { 46 self.0 47 .conn 48 .query_row( 49 "select true from components where entity = ?1", 50 params![self.id()], 51 |_| Ok(()), 52 ) 53 .optional() 54 .map(|o| o.is_some()) 55 .map_err(Error::from) 56 } 57 58 pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> { 59 self.try_created_at().expect("Non-Error") 60 } 61 62 #[tracing::instrument(name = "created_at", level = "debug")] 63 pub fn try_created_at(&self) -> Result<chrono::DateTime<chrono::Utc>, Error> { 64 self.try_component() 65 .map(Option::unwrap_or_default) 66 .map(|CreatedAt(lu)| lu) 67 } 68 69 pub fn last_modified(&self) -> chrono::DateTime<chrono::Utc> { 70 self.try_last_modified().expect("Non-Error") 71 } 72 73 #[tracing::instrument(name = "last_modified", level = "debug")] 74 pub fn try_last_modified(&self) -> Result<chrono::DateTime<chrono::Utc>, Error> { 75 self.try_component() 76 .map(Option::unwrap_or_default) 77 .map(|LastUpdated(lu)| lu) 78 } 79 80 pub fn component_names(&self) -> impl Iterator<Item = String> { 81 self.try_component_names().unwrap() 82 } 83 84 #[tracing::instrument(name = "component_names", level = "debug")] 85 pub fn try_component_names(&self) -> Result<impl Iterator<Item = String>, Error> { 86 let mut stmt = self 87 .0 88 .conn 89 .prepare_cached("select component from components where entity = ?1")?; 90 let names = stmt 91 .query_map(params![self.id()], |row| row.get(0))? 92 .collect::<Result<Vec<_>, _>>()?; 93 Ok(names.into_iter()) 94 } 95 96 pub fn has<B: Bundle>(&self) -> bool { 97 self.try_has::<B>().unwrap() 98 } 99 100 pub fn try_has<B: Bundle>(&self) -> Result<bool, Error> { 101 self.has_all_dynamic(B::COMPONENTS) 102 } 103 104 fn has_all_dynamic(&self, component_names: &[&str]) -> Result<bool, Error> { 105 let mut stmt = self 106 .0 107 .conn 108 .prepare_cached("select true from components where entity = ?1 and component = ?2")?; 109 for name in component_names { 110 if !stmt.exists(params![self.id(), name])? { 111 return Ok(false); 112 } 113 } 114 115 Ok(true) 116 } 117} 118 119impl<'a> Entity<'a> { 120 pub fn destroy(self) { 121 self.try_destroy().unwrap(); 122 } 123 124 #[tracing::instrument(name = "destroy", level = "debug")] 125 pub fn try_destroy(self) -> Result<(), Error> { 126 self.0 127 .conn 128 .execute("delete from components where entity = ?1", [self.id()])?; 129 debug!(entity = self.id(), "destroyed"); 130 Ok(()) 131 } 132} 133 134impl<'a> Entity<'a> { 135 pub fn component<T: Component>(&self) -> Option<T> { 136 match self.try_component::<T>() { 137 Ok(c) => c, 138 Err(e) => panic!("Failed to get Component {}: {e}", T::NAME), 139 } 140 } 141 142 pub fn try_component<T: Component>(&self) -> Result<Option<T>, Error> { 143 let name = T::component_name(); 144 let mut query = self 145 .0 146 .conn 147 .prepare_cached("select data from components where entity = ?1 and component = ?2")?; 148 149 query 150 .query_and_then(params![self.id(), name], |row| { 151 let data = row.get_ref("data")?; 152 Ok(T::from_rusqlite(&rusqlite::types::ToSqlOutput::Borrowed( 153 data, 154 ))?) 155 })? 156 .next() 157 .transpose() 158 } 159} 160 161impl<'a> Entity<'a> { 162 pub fn dyn_component(&self, name: &'a str) -> Option<DynComponent<'a>> { 163 self.try_dyn_component(name).unwrap() 164 } 165 166 pub fn try_dyn_component(&self, name: &'a str) -> Result<Option<DynComponent<'a>>, Error> { 167 let mut query = self 168 .0 169 .conn 170 .prepare_cached("select data from components where entity = ?1 and component = ?2")?; 171 172 query 173 .query_and_then(params![self.id(), name], |row| { 174 let data = row.get("data")?; 175 Ok(DynComponent( 176 name, 177 rusqlite::types::ToSqlOutput::Owned(data), 178 )) 179 })? 180 .next() 181 .transpose() 182 } 183} 184 185impl<'a> Entity<'a> { 186 pub fn modify_component<C: Component + Default>(&self, f: impl FnOnce(&mut C)) -> Self { 187 self.try_modify_component(|c| { 188 f(c); 189 Ok(()) 190 }) 191 .unwrap() 192 } 193 194 // TODO: Race Condition; needs refactoring to make Entity generic over 195 // `rusqlite::Connection` and `rusqlite::Transaction` 196 pub fn try_modify_component<C: Component + Default>( 197 &self, 198 f: impl FnOnce(&mut C) -> Result<(), anyhow::Error>, 199 ) -> Result<Self, ModifyComponentError> { 200 let mut component = self.try_component()?.unwrap_or_default(); 201 f(&mut component).map_err(ModifyComponentError::Fn)?; 202 Ok(self.try_attach(component)?) 203 } 204} 205 206#[derive(thiserror::Error, Debug)] 207pub enum ModifyComponentError { 208 #[error(transparent)] 209 Ecs(#[from] Error), 210 #[error("Error in modify-fun: {0}")] 211 Fn(anyhow::Error), 212} 213 214impl<'a> Entity<'a> { 215 pub fn try_matches<D: query::QueryFilter>(&self) -> Result<bool, Error> { 216 let q = query::Query::<(), D, EntityId>::with_filter(self.db(), self.id()); 217 Ok(q.try_iter()?.next().is_some()) 218 } 219 220 pub fn matches<D: query::QueryFilter>(&self) -> bool { 221 self.try_matches::<D>().unwrap() 222 } 223 224 // pub fn try_matches_filtered<F: query::QueryFilter>(&self, filter: F) -> Result<bool, Error> { 225 // let q = query::Query::<(), F>::new(self.db(), filter); 226 // Ok(q.try_iter()?.next().is_some()) 227 // } 228 229 // pub fn matches_filtered<F: query::QueryFilter>(&self, filter: F) -> bool { 230 // self.try_matches_filtered::<F>(filter).unwrap() 231 // } 232} 233 234impl<'a> Entity<'a> { 235 pub fn attach<B: Bundle>(self, component: B) -> Self { 236 self.try_attach::<B>(component).unwrap() 237 } 238 239 pub fn detach<B: Bundle>(self) -> Self { 240 self.try_detach::<B>().unwrap() 241 } 242 243 #[tracing::instrument(name = "attach", level = "debug", skip_all)] 244 pub fn try_attach<B: Bundle>(self, component: B) -> Result<Self, Error> { 245 let components = B::to_rusqlite(&component)?; 246 247 let mut stmt = self.0.conn.prepare_cached( 248 r#" 249 insert into components (entity, component, data) 250 values (?1, ?2, ?3) 251 on conflict (entity, component) do update 252 set data = excluded.data where data is not excluded.data; 253 "#, 254 )?; 255 256 for (component, data) in components { 257 trace!(params = ?(self.id(), component, &data)); 258 259 if let Some(data) = data { 260 let attached_rows = stmt.execute(params![self.id(), component, data])?; 261 if attached_rows > 0 { 262 debug!(entity = self.id(), component, "attached"); 263 } else { 264 debug!(entity = self.id(), component, "no-op") 265 } 266 } else { 267 debug!(component, ?data, "skipping None"); 268 } 269 } 270 271 Ok(self) 272 } 273 274 #[tracing::instrument(name = "detach", level = "debug")] 275 pub fn try_detach<B: Bundle>(self) -> Result<Self, Error> { 276 let mut stmt = self 277 .0 278 .conn 279 .prepare_cached("delete from components where entity = ?1 and component = ?2")?; 280 281 for component in B::COMPONENTS { 282 let deleted_rows = stmt.execute(params![self.id(), component])?; 283 if deleted_rows > 0 { 284 debug!(entity = self.id(), component, "detached"); 285 } else { 286 debug!(entity = self.id(), component, "no-op") 287 } 288 } 289 290 Ok(self) 291 } 292} 293 294impl<'a> Entity<'a> { 295 pub fn or_none(self) -> Option<Self> { 296 self.exists().then_some(self) 297 } 298} 299 300impl<'a> NewEntity<'a> { 301 pub fn attach<B: Bundle>(self, component: B) -> GenericEntity<'a, WithEntityId> { 302 self.try_attach::<B>(component).unwrap() 303 } 304 305 pub fn detach<B: Bundle>(&mut self) -> &mut Self { 306 self 307 } 308 309 pub fn component_names(&self) -> impl Iterator<Item = String> { 310 std::iter::empty() 311 } 312 313 #[tracing::instrument(name = "attach", level = "debug", skip_all)] 314 pub fn try_attach<B: Bundle>( 315 self, 316 bundle: B, 317 ) -> Result<GenericEntity<'a, WithEntityId>, Error> { 318 let data = B::to_rusqlite(&bundle)?; 319 assert!(!data.is_empty()); 320 321 let mut stmt = self.0.conn.prepare_cached( 322 r#" 323 insert into components (entity, component, data) 324 values ((select coalesce(?1, max(entity)+1, 100) from components), ?2, ?3) 325 on conflict (entity, component) do update set data = excluded.data 326 returning entity 327 "#, 328 )?; 329 330 let mut eid = None; 331 for (component, data) in data { 332 trace!(params = ?(eid, component, &data)); 333 334 if let Some(data) = data { 335 eid = Some(stmt.query_row(params![eid, component, data], |row| { 336 row.get::<_, EntityId>("entity") 337 })?); 338 339 debug!(entity = eid.unwrap(), component, "attached"); 340 } else { 341 debug!(component, ?data, "skipping None"); 342 } 343 } 344 345 let Some(eid) = eid else { 346 panic!("Bundle::to_rusqlite returned zero rows. That shouldn't happen.") 347 }; 348 349 let entity = GenericEntity(self.0, WithEntityId(eid)); 350 351 Ok(entity) 352 } 353 354 #[tracing::instrument(name = "detach", level = "debug", skip_all)] 355 pub fn try_detach<B: Bundle>(&mut self) -> Result<&mut Self, Error> { 356 Ok(self) 357 } 358 359 #[tracing::instrument(name = "component_names", level = "debug")] 360 pub fn try_component_names(&self) -> Result<impl Iterator<Item = String>, Error> { 361 Ok(std::iter::empty()) 362 } 363} 364 365impl<'a> NewEntity<'a> { 366 pub fn modify_component<C: Component + Default>(&self, f: impl FnOnce(&mut C)) -> Entity<'a> { 367 self.try_modify_component(|c| { 368 f(c); 369 Ok(()) 370 }) 371 .unwrap() 372 } 373 374 // TODO: Race Condition; needs refactoring to make Entity generic over 375 // `rusqlite::Connection` and `rusqlite::Transaction` 376 pub fn try_modify_component<C: Component + Default>( 377 &self, 378 f: impl FnOnce(&mut C) -> Result<(), anyhow::Error>, 379 ) -> Result<Entity<'a>, ModifyComponentError> { 380 let mut component = C::default(); 381 f(&mut component).map_err(ModifyComponentError::Fn)?; 382 Ok(self.try_attach(component)?) 383 } 384} 385 386impl<'a> std::fmt::Display for NewEntity<'a> { 387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 388 f.debug_tuple("Entity").field(&"nil").finish() 389 } 390} 391 392impl<'a> std::fmt::Display for Entity<'a> { 393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 394 f.debug_tuple("Entity").field(&(self.1).0).finish() 395 } 396} 397 398impl<'a> std::fmt::Debug for NewEntity<'a> { 399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 400 f.debug_tuple("Entity").field(&"nil").finish() 401 } 402} 403 404impl<'a> std::fmt::Debug for Entity<'a> { 405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 406 f.debug_tuple("Entity").field(&(self.1).0).finish() 407 } 408}