Experiments in applying Entity-Component-System patterns to durable data storage APIs.
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}