Skip to content

JViggiani/bevy_persistence_database

Repository files navigation

bevy_persistence_database

Persistence for Bevy ECS to ArangoDB or Postgres with an idiomatic Bevy Query API, explicit load triggers, and manual commits you can await.

Highlights

  • PersistenceQuery mirrors Bevy Query: use iter/get/single/get_many/iter_combinations after calling ensure_loaded().
  • Smart caching and coalesced loads within a frame; force_refresh() bypasses cache when needed.
  • Presence/value filters: With, Without, Or, optionals, comparisons, and key filters via Guid::key_field().
  • Resources persisted alongside components with #[persist(resource)].
  • Batching + parallel commit execution; per-document versioning for optimistic concurrency.

Install

[dependencies]
bevy = { version = "0.17", default-features = false, features = ["bevy_log"] }
bevy_persistence_database = { version = "0.2.2", features = ["arango", "postgres"] }

Enable arango or postgres features based on your backend and supply an Arc<dyn DatabaseConnection> at startup.

Define persistable types

use bevy_persistence_database::persist;

#[persist(component)]
#[derive(Clone)]
pub struct Health { pub value: i32 }

#[persist(resource)]
#[derive(Clone)]
pub struct GameSettings { pub difficulty: f32, pub map_name: String }

Add the plugin

use bevy::prelude::*;
use bevy_persistence_database::{PersistencePlugins, persistence_plugin::PersistencePluginConfig};
use std::sync::Arc;

fn main() {
    let db: Arc<dyn bevy_persistence_database::DatabaseConnection> = /* connect backend */;

    App::new()
        .add_plugins(PersistencePlugins::new(db).with_config(PersistencePluginConfig {
            default_store: "example".into(),
            ..Default::default()
        }))
        .run();
}

Loading data

use bevy::prelude::*;
use bevy_persistence_database::{Guid, PersistenceQuery};

fn system(mut pq: PersistenceQuery<(&Health, Option<&Position>)>) {
    let count = pq
        .store("example") // optional override of default_store
        .where(Guid::key_field().eq("player-1"))
        .ensure_loaded()
        .iter()
        .count();
    info!("loaded {} entities", count);
}

After ensure_loaded(), PersistenceQuery derefs to a regular Bevy Query for pass-through reads without additional DB I/O. Use force_refresh() to bypass cache.

Joins and transmute

use bevy::prelude::*;
use bevy_persistence_database::{PersistenceQuery, query::join::Join, query::QueryDataToComponents};

fn join_example(
    mut common: PersistenceQuery<(&Health, &Position)>,
    mut names: PersistenceQuery<&PlayerName>,
) {
    let joined = names.join_filtered(&mut common).ensure_loaded();
    for (_e, (health, position, name)) in joined.iter() {
        info!("{} @ ({}, {})", name.name, position.x, position.y);
    }
}

fn transmute_example(mut pq: PersistenceQuery<&Health>) {
    pq.ensure_loaded();
    let comps = pq.transmute::<(&Health, Option<&Position>)>();
    for (_e, (h, pos)) in comps.iter() {
        let _ = (h.value, pos.map(|p| p.x));
    }
}

Use join_filtered to correlate data across multiple queries without reloading, and transmute to widen the component view for reuse in systems or for table-style assertions in tests.

Committing changes

Changes are not auto-committed. Use the helpers:

use bevy_persistence_database::{commit, commit_sync};

// Async (drives its own updates internally)
let _ = commit(&mut app, db.clone(), "example").await?;

// Blocking convenience
let _ = commit_sync(&mut app, db.clone(), "example")?;

Or trigger manually if you’re already inside a running app:

use bevy_persistence_database::plugins::{register_commit_listener, TriggerCommit};
use tokio::sync::oneshot;

let correlation_id = job.operation_id; // choose your own handle
let (tx, rx) = oneshot::channel();
register_commit_listener(app.world_mut(), correlation_id, tx);

app.world_mut().write_message(TriggerCommit {
    correlation_id: Some(correlation_id),
    target_connection: db.clone(),
    store: "example".into(),
});

// hold `rx` to await the commit result in your orchestrator

Listeners are just oneshot senders keyed by a correlation ID. Each TriggerCommit should use a unique ID (you can reuse your job/operation ID) so the completion is routed to the right waiter. The plugin cleans up the entry when it sends the result.

Advanced configuration

use bevy_persistence_database::{PersistencePlugins, persistence_plugin::PersistencePluginConfig};

let config = PersistencePluginConfig {
    batching_enabled: true,
    commit_batch_size: 500,
    thread_count: 4,
    default_store: "example".into(),
};

app.add_plugins(PersistencePlugins::new(db.clone()).with_config(config));
  • batching_enabled/commit_batch_size: control commit chunking and parallel execution.
  • thread_count: Rayon pool size used for commit preparation.
  • default_store: fallback store when queries/commits don’t override .store().

Scheduling notes

  • Loads can run in Update or PostUpdate.
  • Deferred world mutations from loads are applied before PersistenceSystemSet::PreCommit.
  • Commit pipeline runs in PersistenceSystemSet::Commit; readers that need fresh data should run after PreCommit.

Error handling

All public APIs return Result<_, PersistenceError>. Version conflicts, connection issues, and timeouts surface through that error type so you can decide whether to retry, fail the job, or surface an error to callers.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors