···11+# `ecsdb`
22+33+Experiments in applying Entity-Component-System patterns to durable data storage APIs.
44+55+## Usage
66+77+```rust
88+use ecsdb::*;
99+use ecsdb::query::*;
1010+use serde::{Serialize, Deserialize};
1111+1212+#[derive(Debug, Component, Serialize, Deserialize)]
1313+struct Headline(String);
1414+1515+#[derive(Debug, Component, Serialize, Deserialize)]
1616+struct Date(String);
1717+1818+let ecs = Ecs::open_in_memory().unwrap();
1919+ecs.new_entity()
2020+ .attach(Headline("My Note".into()))
2121+ .attach(Date(chrono::Utc::now().to_rfc3339()));
2222+2323+ecs.new_entity().attach(Headline("My Note".into()));
2424+2525+for (entity, headline) in ecs.query::<(Entity, Headline), Without<Date>>().into_iter() {
2626+ println!(
2727+ "Entity '{}' (id={}) is missing component 'Date'",
2828+ headline.0,
2929+ entity.id()
3030+ );
3131+3232+ entity.destroy();
3333+}
3434+```
3535+3636+## Components
3737+3838+A component is a singular piece of data, similar to a column in a relational
3939+database.
4040+4141+They must implement `serde::Serialize`, `serde::Deserialize` and
4242+`ecsdb::Component`, all of which can be `#[derive]`'d.
4343+4444+```rust
4545+# use serde::{Serialize, Deserialize};
4646+# use ecsdb::Component;
4747+4848+#[derive(Serialize, Deserialize, Component)]
4949+pub struct Marker;
5050+5151+#[derive(Serialize, Deserialize, Component)]
5252+pub struct Date(chrono::DateTime<chrono::Utc>);
5353+5454+#[derive(Serialize, Deserialize, Component)]
5555+pub enum State {
5656+ New,
5757+ Processing,
5858+ Finished
5959+}
6060+```
6161+6262+## Entities
6363+6464+```rust
6565+# use ecsdb::{Component, Ecs, query::*};
6666+# use serde::{Serialize, Deserialize};
6767+# use ecsdb::doctests::*;
6868+6969+# let ecs = Ecs::open_in_memory().unwrap();
7070+7171+// Attach components via `Entity::attach`:
7272+let entity = ecs.new_entity()
7373+ .attach(State::New);
7474+7575+// To retrieve an attached component, use `Entity::component`:
7676+let date: Option<Date> = entity.component::<Date>();
7777+7878+// To detach a component, use `Entity::detach`. Detaching a non-attached component is a no-op:
7979+entity.detach::<Date>();
8080+8181+// Re-attaching a component of the same type overwrites the old. Attaching the
8282+// same value is a no-op:
8383+entity.attach(State::Finished);
8484+```
8585+8686+## Systems
8787+8888+Systems are functions operating on an `Ecs`. They can be registerd via
8989+`Ecs::register` and run via `Ecs::tick`. They can take a set of 'magic'
9090+parameters to access data in the `Ecs`:
9191+9292+```rust
9393+# use ecsdb::doctests::*;
9494+use ecsdb::query::{Query, With, Without};
9595+9696+// This system will attach `State::New` to all entities that have a `Marker` but
9797+// no `State` component
9898+fn process_marked_system(marked_entities: Query<Entity, (With<Marker>, Without<State>)>) {
9999+ for entity in marked_entities.iter() {
100100+ entity
101101+ .attach(State::New)
102102+ .detach::<Marker>();
103103+ }
104104+}
105105+106106+// This system logs all entities that have both `Date` and `Marker` but no
107107+// `State`
108108+fn log_system(entities: Query<(EntityId, Date, Marker), Without<State>>) {
109109+ for (entity_id, Date(date), _marker) in entities.iter() {
110110+ println!("{entity_id} {date}");
111111+ }
112112+}
113113+114114+let ecs = Ecs::open_in_memory().unwrap();
115115+ecs.run_system(process_marked_system).unwrap();
116116+ecs.run_system(log_system).unwrap();
117117+```
118118+119119+## Scheduling
120120+121121+`ecsdb::Schedule` allows scheduling of different systems by different criterias:
122122+123123+```rust
124124+# use ecsdb::doctests::*;
125125+# let ecs = Ecs::open_in_memory().unwrap();
126126+127127+fn sys_a() {}
128128+fn sys_b() {}
129129+130130+use ecsdb::schedule::*;
131131+let mut schedule = Schedule::new();
132132+133133+// Run `sys_a` every 15 minutes
134134+schedule.add(sys_a, Every(chrono::Duration::minutes(15)));
135135+136136+// Run `sys_b` after `sys_a`
137137+schedule.add(sys_b, After::system(sys_a));
138138+139139+// Run all pending systems
140140+schedule.tick(&ecs);
141141+```
142142+143143+- `schedule::Every(Duration)` runs a system periodically
144144+- `schedule::After` runs one system after another finished
145145+- `schedule::Once` runs a system once per database
146146+- `schedule::Always` runs a system on every `Schedule::tick`
···11+macro_rules! for_each_tuple {
22+ ( $m:ident; ) => { };
33+44+ ( $m:ident; $h:ident, $($t:ident,)* ) => (
55+ $m!($h $($t)*);
66+ crate::tuple_macros::for_each_tuple! { $m; $($t,)* }
77+ );
88+99+ ( $m:ident ) => {
1010+ crate::tuple_macros::for_each_tuple! { $m; A, B, C, D, E, F, G, H, I, J, K, L, M, O, P, Q, }
1111+ };
1212+}
1313+1414+pub(crate) use for_each_tuple;
+106-7
src/schema.sql
···11+-- Components
12create table if not exists components (
23 entity integer not null,
34 component text not null,
55+ data blob
66+);
4755-66-77-88-88+create unique index if not exists components_entity_component_unqiue_idx on components (entity, component);
991010create index if not exists components_component_idx on components (component);
11111212-create trigger if not exists components_last_modified_trigger before
1313-update on components for each row begin
1414-update components
1212+create view if not exists entity_components (entity, components) as
1313+select
1414+ entity,
1515+ json_group_array (component)
1616+from
1717+ components
1818+group by
1919+ entity
2020+order by
2121+ component asc;
2222+2323+-- Set ecsdb::CreatedAt on initial insert
2424+create trigger if not exists components_created_insert_trigger
2525+after insert on components
2626+for each row
2727+when new.component != 'ecsdb::CreatedAt' begin
2828+insert into
2929+ components (entity, component, data)
3030+values
3131+ (
3232+ new.entity,
3333+ 'ecsdb::CreatedAt',
3434+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
3535+ ) on conflict do nothing;
3636+end;
3737+3838+-- Update ecsdb::LastUpdated on update
3939+create trigger if not exists components_last_modified_update_trigger after
4040+update on components for each row when new.component != 'ecsdb::LastUpdated' begin
4141+insert into
4242+ components (entity, component, data)
4343+values
4444+ (
4545+ new.entity,
4646+ 'ecsdb::LastUpdated',
4747+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
4848+ ) on conflict (entity, component) do
4949+update
5050+set
5151+ data = excluded.data;
5252+end;
5353+5454+-- Update ecsdb::LastUpdated on insert
5555+create trigger if not exists components_last_modified_insert_trigger after insert on components for each row when new.component != 'ecsdb::LastUpdated' begin
5656+insert into
5757+ components (entity, component, data)
5858+values
5959+ (
6060+ new.entity,
6161+ 'ecsdb::LastUpdated',
6262+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
6363+ ) on conflict (entity, component) do
6464+update
6565+set
6666+ data = excluded.data;
6767+end;
6868+6969+-- Update ecsdb::LastUpdated on delete, except when it's the last component
7070+create trigger if not exists components_last_modified_delete_trigger after delete on components for each row when old.component != 'ecsdb::LastUpdated'
7171+and (
7272+ select
7373+ true
7474+ from
7575+ entity_components
7676+ where
7777+ entity = old.entity
7878+ and components != json_array ('ecsdb::LastUpdated')
7979+) begin
8080+insert into
8181+ components (entity, component, data)
8282+values
8383+ (
8484+ old.entity,
8585+ 'ecsdb::LastUpdated',
8686+ json_quote (strftime ('%Y-%m-%dT%H:%M:%fZ'))
8787+ ) on conflict (entity, component) do
8888+update
8989+set
9090+ data = excluded.data;
9191+9292+end;
9393+9494+-- Delete ecsdb::LastUpdated when it's the last remaining component
9595+create trigger if not exists components_last_modified_delete_last_component_trigger after delete on components for each row when (
9696+ select
9797+ true
9898+ from
9999+ entity_components
100100+ where
101101+ entity = old.entity
102102+ and components = json_array ('ecsdb::LastUpdated')
103103+) begin
104104+delete from components
105105+where
106106+ entity = old.entity
107107+ and component = 'ecsdb::LastUpdated';
108108+end;
109109+110110+-- Resources
111111+create table if not exists resources (
112112+ name text not null unique,
113113+ data blob,
···11+---
22+source: src/query/ir.rs
33+description: "WithoutComponent(\"ecsdb::Test\").simplify().sql_query()"
44+---
55+SqlFragment<ecsdb::query::ir::Select> {
66+ sql: "select distinct entity from components where (select true from components c2 where c2.entity = components.entity and c2.component = ?1) is null",
77+ placeholders: [
88+ (
99+ "?1",
1010+ Text(
1111+ "ecsdb::Test",
1212+ ),
1313+ ),
1414+ ],
1515+}
+15
.zed/settings.json
···11+// Folder-specific settings
22+//
33+// For a full list of overridable settings, and general information on folder-specific settings,
44+// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
55+{
66+ "lsp": {
77+ "rust-analyzer": {
88+ "initialization_options": {
99+ "check": {
1010+ "command": "clippy" // rust-analyzer.checkOnSave.command
1111+ }
1212+ }
1313+ }
1414+ }
1515+}