···1+# `ecsdb`
2+3+Experiments in applying Entity-Component-System patterns to durable data storage APIs.
4+5+## Usage
6+7+```rust
8+use ecsdb::*;
9+use ecsdb::query::*;
10+use serde::{Serialize, Deserialize};
11+12+#[derive(Debug, Component, Serialize, Deserialize)]
13+struct Headline(String);
14+15+#[derive(Debug, Component, Serialize, Deserialize)]
16+struct Date(String);
17+18+let ecs = Ecs::open_in_memory().unwrap();
19+ecs.new_entity()
20+ .attach(Headline("My Note".into()))
21+ .attach(Date(chrono::Utc::now().to_rfc3339()));
22+23+ecs.new_entity().attach(Headline("My Note".into()));
24+25+for (entity, headline) in ecs.query::<(Entity, Headline), Without<Date>>().into_iter() {
26+ println!(
27+ "Entity '{}' (id={}) is missing component 'Date'",
28+ headline.0,
29+ entity.id()
30+ );
31+32+ entity.destroy();
33+}
34+```
35+36+## Components
37+38+A component is a singular piece of data, similar to a column in a relational
39+database.
40+41+They must implement `serde::Serialize`, `serde::Deserialize` and
42+`ecsdb::Component`, all of which can be `#[derive]`'d.
43+44+```rust
45+# use serde::{Serialize, Deserialize};
46+# use ecsdb::Component;
47+48+#[derive(Serialize, Deserialize, Component)]
49+pub struct Marker;
50+51+#[derive(Serialize, Deserialize, Component)]
52+pub struct Date(chrono::DateTime<chrono::Utc>);
53+54+#[derive(Serialize, Deserialize, Component)]
55+pub enum State {
56+ New,
57+ Processing,
58+ Finished
59+}
60+```
61+62+## Entities
63+64+```rust
65+# use ecsdb::{Component, Ecs, query::*};
66+# use serde::{Serialize, Deserialize};
67+# use ecsdb::doctests::*;
68+69+# let ecs = Ecs::open_in_memory().unwrap();
70+71+// Attach components via `Entity::attach`:
72+let entity = ecs.new_entity()
73+ .attach(State::New);
74+75+// To retrieve an attached component, use `Entity::component`:
76+let date: Option<Date> = entity.component::<Date>();
77+78+// To detach a component, use `Entity::detach`. Detaching a non-attached component is a no-op:
79+entity.detach::<Date>();
80+81+// Re-attaching a component of the same type overwrites the old. Attaching the
82+// same value is a no-op:
83+entity.attach(State::Finished);
84+```
85+86+## Systems
87+88+Systems are functions operating on an `Ecs`. They can be registerd via
89+`Ecs::register` and run via `Ecs::tick`. They can take a set of 'magic'
90+parameters to access data in the `Ecs`:
91+92+```rust
93+# use ecsdb::doctests::*;
94+use ecsdb::query::{Query, With, Without};
95+96+// This system will attach `State::New` to all entities that have a `Marker` but
97+// no `State` component
98+fn process_marked_system(marked_entities: Query<Entity, (With<Marker>, Without<State>)>) {
99+ for entity in marked_entities.iter() {
100+ entity
101+ .attach(State::New)
102+ .detach::<Marker>();
103+ }
104+}
105+106+// This system logs all entities that have both `Date` and `Marker` but no
107+// `State`
108+fn log_system(entities: Query<(EntityId, Date, Marker), Without<State>>) {
109+ for (entity_id, Date(date), _marker) in entities.iter() {
110+ println!("{entity_id} {date}");
111+ }
112+}
113+114+let ecs = Ecs::open_in_memory().unwrap();
115+ecs.run_system(process_marked_system).unwrap();
116+ecs.run_system(log_system).unwrap();
117+```
118+119+## Scheduling
120+121+`ecsdb::Schedule` allows scheduling of different systems by different criterias:
122+123+```rust
124+# use ecsdb::doctests::*;
125+# let ecs = Ecs::open_in_memory().unwrap();
126+127+fn sys_a() {}
128+fn sys_b() {}
129+130+use ecsdb::schedule::*;
131+let mut schedule = Schedule::new();
132+133+// Run `sys_a` every 15 minutes
134+schedule.add(sys_a, Every(chrono::Duration::minutes(15)));
135+136+// Run `sys_b` after `sys_a`
137+schedule.add(sys_b, After::system(sys_a));
138+139+// Run all pending systems
140+schedule.tick(&ecs);
141+```
142+143+- `schedule::Every(Duration)` runs a system periodically
144+- `schedule::After` runs one system after another finished
145+- `schedule::Once` runs a system once per database
146+- `schedule::Always` runs a system on every `Schedule::tick`
···1+macro_rules! for_each_tuple {
2+ ( $m:ident; ) => { };
3+4+ ( $m:ident; $h:ident, $($t:ident,)* ) => (
5+ $m!($h $($t)*);
6+ crate::tuple_macros::for_each_tuple! { $m; $($t,)* }
7+ );
8+9+ ( $m:ident ) => {
10+ crate::tuple_macros::for_each_tuple! { $m; A, B, C, D, E, F, G, H, I, J, K, L, M, O, P, Q, }
11+ };
12+}
13+14+pub(crate) use for_each_tuple;
+106-7
src/schema.sql
···01create table if not exists components (
2 entity integer not null,
3 component text not null,
0045-6-7-8-910create index if not exists components_component_idx on components (component);
1112-create trigger if not exists components_last_modified_trigger before
13-update on components for each row begin
14-update components
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···1+-- Components
2create table if not exists components (
3 entity integer not null,
4 component text not null,
5+ data blob
6+);
78+create unique index if not exists components_entity_component_unqiue_idx on components (entity, component);
000910create index if not exists components_component_idx on components (component);
1112+create view if not exists entity_components (entity, components) as
13+select
14+ entity,
15+ json_group_array (component)
16+from
17+ components
18+group by
19+ entity
20+order by
21+ component asc;
22+23+-- Set ecsdb::CreatedAt on initial insert
24+create trigger if not exists components_created_insert_trigger
25+after insert on components
26+for each row
27+when new.component != 'ecsdb::CreatedAt' begin
28+insert into
29+ components (entity, component, data)
30+values
31+ (
32+ new.entity,
33+ 'ecsdb::CreatedAt',
34+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
35+ ) on conflict do nothing;
36+end;
37+38+-- Update ecsdb::LastUpdated on update
39+create trigger if not exists components_last_modified_update_trigger after
40+update on components for each row when new.component != 'ecsdb::LastUpdated' begin
41+insert into
42+ components (entity, component, data)
43+values
44+ (
45+ new.entity,
46+ 'ecsdb::LastUpdated',
47+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
48+ ) on conflict (entity, component) do
49+update
50+set
51+ data = excluded.data;
52+end;
53+54+-- Update ecsdb::LastUpdated on insert
55+create trigger if not exists components_last_modified_insert_trigger after insert on components for each row when new.component != 'ecsdb::LastUpdated' begin
56+insert into
57+ components (entity, component, data)
58+values
59+ (
60+ new.entity,
61+ 'ecsdb::LastUpdated',
62+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
63+ ) on conflict (entity, component) do
64+update
65+set
66+ data = excluded.data;
67+end;
68+69+-- Update ecsdb::LastUpdated on delete, except when it's the last component
70+create trigger if not exists components_last_modified_delete_trigger after delete on components for each row when old.component != 'ecsdb::LastUpdated'
71+and (
72+ select
73+ true
74+ from
75+ entity_components
76+ where
77+ entity = old.entity
78+ and components != json_array ('ecsdb::LastUpdated')
79+) begin
80+insert into
81+ components (entity, component, data)
82+values
83+ (
84+ old.entity,
85+ 'ecsdb::LastUpdated',
86+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
87+ ) on conflict (entity, component) do
88+update
89+set
90+ data = excluded.data;
91+92+end;
93+94+-- Delete ecsdb::LastUpdated when it's the last remaining component
95+create trigger if not exists components_last_modified_delete_last_component_trigger after delete on components for each row when (
96+ select
97+ true
98+ from
99+ entity_components
100+ where
101+ entity = old.entity
102+ and components = json_array ('ecsdb::LastUpdated')
103+) begin
104+delete from components
105+where
106+ entity = old.entity
107+ and component = 'ecsdb::LastUpdated';
108+end;
109+110+-- Resources
111+create table if not exists resources (
112+ name text not null unique,
113+ data blob,
···1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3-version = 3
45[[package]]
6name = "ecsdb_derive"
00000000000000000000000000000000000000000
···1+---
2+source: src/query/ir.rs
3+description: "WithoutComponent(\"ecsdb::Test\").simplify().sql_query()"
4+---
5+SqlFragment<ecsdb::query::ir::Select> {
6+ sql: "select distinct entity from components where (select true from components c2 where c2.entity = components.entity and c2.component = ?1) is null",
7+ placeholders: [
8+ (
9+ "?1",
10+ Text(
11+ "ecsdb::Test",
12+ ),
13+ ),
14+ ],
15+}
+15
.zed/settings.json
···000000000000000
···1+// Folder-specific settings
2+//
3+// For a full list of overridable settings, and general information on folder-specific settings,
4+// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5+{
6+ "lsp": {
7+ "rust-analyzer": {
8+ "initialization_options": {
9+ "check": {
10+ "command": "clippy" // rust-analyzer.checkOnSave.command
11+ }
12+ }
13+ }
14+ }
15+}