bevy_persistence_database/bevy/query/
persistence_query.rs1use crate::bevy::plugins::persistence_plugin::{PersistencePluginConfig, TokioRuntime};
4use crate::core::db::connection::{DatabaseConnectionResource, DocumentKind};
5use crate::core::db::{DatabaseConnection, read_version};
6use crate::core::persist::Persist;
7use crate::core::query::{FilterExpression, PersistenceQuerySpecification};
8use crate::core::session::PersistenceSession;
9use crate::core::versioning::version_manager::VersionKey;
10use bevy::prelude::{Component, Entity, World};
11use std::sync::Arc;
12
13pub struct PersistenceQuery {
15 db: Option<Arc<dyn DatabaseConnection>>,
16 store: Option<String>,
17 pub component_names: Vec<&'static str>,
18 filter_expr: Option<FilterExpression>,
19
20 pub(crate) without_component_names: Vec<&'static str>,
22
23 pub(crate) fetch_only_component_names: Vec<&'static str>,
25
26 force_full_docs: bool,
28}
29
30impl PersistenceQuery {
31 pub fn new() -> Self {
39 Self {
40 db: None,
41 store: None,
42 component_names: Vec::new(),
43 filter_expr: None,
44 without_component_names: Vec::new(),
45 fetch_only_component_names: Vec::new(),
46 force_full_docs: false,
47 }
48 }
49
50 pub fn with_db(mut self, db: Arc<dyn DatabaseConnection>) -> Self {
55 self.db = Some(db);
56 self
57 }
58
59 pub fn store(mut self, store: impl Into<String>) -> Self {
63 self.store = Some(store.into());
64 self
65 }
66
67 pub fn with<T: Component + Persist>(mut self) -> Self {
69 self.component_names.push(T::name());
70 self
71 }
72
73 pub fn without<T: Component + Persist>(mut self) -> Self {
75 self.without_component_names.push(T::name());
76 self
77 }
78
79 pub fn fetch_only_component(mut self, component_name: &'static str) -> Self {
81 self.fetch_only_component_names.push(component_name);
82 self
83 }
84
85 pub fn or(mut self, expression: FilterExpression) -> Self {
87 self.filter_expr = Some(match self.filter_expr.take() {
88 Some(existing) => existing.or(expression),
89 None => expression,
90 });
91 self
92 }
93
94 pub fn by_guids(mut self, guids: Vec<String>) -> Self {
99 let key_filter = FilterExpression::DocumentKey.in_(guids);
100 self.filter_expr = Some(match self.filter_expr.take() {
101 Some(existing) => existing.and(key_filter),
102 None => key_filter,
103 });
104 self
105 }
106
107 pub fn run(mut self, world: &mut World) -> Vec<Entity> {
115 if self.db.is_none() {
116 self.db = Some(
117 world
118 .resource::<DatabaseConnectionResource>()
119 .connection
120 .clone(),
121 );
122 }
123 if self.store.is_none() {
124 self.store = Some(
125 world
126 .resource::<PersistencePluginConfig>()
127 .default_store
128 .clone(),
129 );
130 }
131 let runtime = world.resource::<TokioRuntime>().runtime.clone();
132 runtime.block_on(self.fetch_into(world))
133 }
134
135 pub fn filter(mut self, expression: FilterExpression) -> Self {
137 fn collect(expr: &FilterExpression, names: &mut Vec<&'static str>) {
138 match expr {
139 FilterExpression::Field { component_name, .. } => {
140 if !names.contains(component_name) {
141 names.push(component_name);
142 }
143 }
144 FilterExpression::DocumentKey => {}
145 FilterExpression::BinaryOperator { lhs, rhs, .. } => {
146 collect(lhs, names);
147 collect(rhs, names);
148 }
149 FilterExpression::Literal(_) => {}
150 }
151 }
152
153 let mut names = self.component_names.clone();
154 collect(&expression, &mut names);
155 self.component_names = names;
156
157 self.filter_expr = Some(expression);
158 self
159 }
160
161 #[cfg(test)]
163 pub fn for_component<T: Component + Persist>(mut self) -> Self {
164 self.component_names.push(T::name());
165 self
166 }
167
168 pub fn build_spec(&self) -> PersistenceQuerySpecification {
170 let mut fetch_only = self.component_names.clone();
171 fetch_only.extend(self.fetch_only_component_names.iter().copied());
172 fetch_only.sort_unstable();
173 fetch_only.dedup();
174
175 let presence_with = self.component_names.clone();
176 let presence_without = self.without_component_names.clone();
177 let value_filters = self.filter_expr.clone();
178 let force_full_docs = self.force_full_docs;
179
180 let spec = PersistenceQuerySpecification {
181 store: self.store.clone().unwrap_or_default(),
182 kind: DocumentKind::Entity,
183 presence_with: presence_with.clone(),
184 presence_without: presence_without.clone(),
185 fetch_only: fetch_only.clone(),
186 value_filters: value_filters.clone(),
187 return_full_docs: force_full_docs
188 || (presence_with.is_empty() && presence_without.is_empty()),
189 pagination: None,
190 };
191
192 bevy::log::debug!(
193 "[builder] build_spec full_docs={} presence_with={:?} without={:?} fetch_only={:?} filter={:?}",
194 force_full_docs,
195 spec.presence_with,
196 spec.presence_without,
197 spec.fetch_only,
198 spec.value_filters
199 );
200
201 spec
202 }
203
204 pub async fn fetch_ids(&self) -> Vec<String> {
206 let db = self
207 .db
208 .as_ref()
209 .expect("PersistenceQuery: call with_db() before fetch_ids()");
210 let spec = self.build_spec();
211 db.execute_keys(&spec).await.expect("query failed")
212 }
213
214 pub async fn fetch_into(&self, world: &mut World) -> Vec<bevy::prelude::Entity> {
216 let db = self
217 .db
218 .as_ref()
219 .expect("PersistenceQuery: call with_db() or use run() before fetch_into()");
220 let store = self
221 .store
222 .as_deref()
223 .expect("PersistenceQuery: call store() or use run() before fetch_into()");
224
225 let mut session = world
226 .remove_resource::<PersistenceSession>()
227 .expect("PersistenceSession missing");
228
229 let spec = self.build_spec();
230
231 bevy::log::debug!(
232 "[builder] fetch_into issuing execute_documents (partial_projection={})",
233 !spec.return_full_docs
234 );
235 let documents = db
236 .execute_documents(&spec)
237 .await
238 .expect("Batch document fetch failed");
239 bevy::log::debug!(
240 "[builder] fetch_into: backend returned {} documents",
241 documents.len()
242 );
243
244 let mut result = Vec::with_capacity(documents.len());
245 if !documents.is_empty() {
246 let mut explicit_components: Vec<&'static str> = self.component_names.clone();
249 explicit_components.extend(self.fetch_only_component_names.iter().copied());
250 explicit_components.sort_unstable();
251 explicit_components.dedup();
252
253 for doc in documents {
254 let key_field = db.document_key_field();
255 let key = doc[key_field].as_str().unwrap_or_default().to_string();
256 if key.is_empty() {
257 bevy::log::debug!(
258 "[builder] fetch_into: skipping doc missing key '{}'",
259 key_field
260 );
261 continue;
262 }
263
264 bevy::log::trace!(
265 "[builder] deserializing {:?} for key={}",
266 explicit_components,
267 key
268 );
269 let entity = session
270 .materialize_entity_document(
271 world,
272 &doc,
273 key_field,
274 &explicit_components,
275 true,
276 )
277 .expect("component deserialization failed")
278 .expect("document key should be present");
279
280 result.push(entity);
281 }
282 }
283
284 session
285 .fetch_and_insert_resources(&**db, store, world)
286 .await
287 .expect("resource deserialization failed");
288
289 world.insert_resource(session);
290 bevy::log::debug!(
291 "[builder] fetch_into: inserted {} entities into world",
292 result.len()
293 );
294 result
295 }
296
297 pub fn reconcile_versions(mut self, world: &mut World) -> usize {
311 if self.db.is_none() {
312 self.db = Some(
313 world
314 .resource::<DatabaseConnectionResource>()
315 .connection
316 .clone(),
317 );
318 }
319 if self.store.is_none() {
320 self.store = Some(
321 world
322 .resource::<PersistencePluginConfig>()
323 .default_store
324 .clone(),
325 );
326 }
327 let runtime = world.resource::<TokioRuntime>().runtime.clone();
328 runtime.block_on(self.reconcile_versions_into(world))
329 }
330
331 async fn reconcile_versions_into(&self, world: &mut World) -> usize {
332 let db = self
333 .db
334 .as_ref()
335 .expect("PersistenceQuery: call with_db() or use run() before reconcile_versions()");
336
337 let mut query_with_full_docs = self.clone();
338 query_with_full_docs.force_full_docs = true;
339 let spec = query_with_full_docs.build_spec();
340
341 let documents = match db.execute_documents(&spec).await {
342 Ok(docs) => docs,
343 Err(e) => {
344 bevy::log::error!("version reconcile: document fetch failed: {e}");
345 return 0;
346 }
347 };
348
349 let mut session = world
350 .remove_resource::<PersistenceSession>()
351 .expect("PersistenceSession missing");
352
353 let key_field = db.document_key_field();
354 let store = self
355 .store
356 .as_deref()
357 .expect("PersistenceQuery: call store() or use run() before reconcile_versions()");
358 let mut realigned = 0usize;
359 for doc in &documents {
360 let key = doc[key_field].as_str().unwrap_or_default().to_string();
361 if key.is_empty() {
362 continue;
363 }
364 let version = read_version(doc).unwrap_or(1);
365 session
366 .version_manager_mut()
367 .set_version(VersionKey::Entity(key), version);
368 realigned += 1;
369 }
370
371 let resource_types: Vec<_> = session.persisted_resource_types().collect();
372 for type_id in resource_types {
373 let Some(res_name) = session.resource_name_for_type(type_id) else {
374 continue;
375 };
376 if let Ok(Some((_, version))) = db.fetch_resource(store, res_name).await {
377 session
378 .version_manager_mut()
379 .set_version(VersionKey::Resource(type_id), version);
380 realigned += 1;
381 }
382 }
383
384 world.insert_resource(session);
385 bevy::log::info!(
386 "version reconcile: realigned {realigned} entity/resource versions from the database"
387 );
388 realigned
389 }
390}
391
392impl Clone for PersistenceQuery {
393 fn clone(&self) -> Self {
394 Self {
395 db: self.db.clone(),
396 store: self.store.clone(),
397 component_names: self.component_names.clone(),
398 filter_expr: self.filter_expr.clone(),
399 without_component_names: self.without_component_names.clone(),
400 fetch_only_component_names: self.fetch_only_component_names.clone(),
401 force_full_docs: self.force_full_docs,
402 }
403 }
404}
405
406impl Default for PersistenceQuery {
407 fn default() -> Self {
408 Self::new()
409 }
410}
411
412pub trait WithComponentExt {
414 fn with_component(self, component_name: &'static str) -> Self;
415 fn without_component(self, component_name: &'static str) -> Self;
416 fn fetch_only_component(self, component_name: &'static str) -> Self;
417}
418
419impl WithComponentExt for PersistenceQuery {
420 fn with_component(mut self, component_name: &'static str) -> Self {
421 self.component_names.push(component_name);
422 self
423 }
424
425 fn without_component(mut self, component_name: &'static str) -> Self {
426 self.without_component_names.push(component_name);
427 self
428 }
429
430 fn fetch_only_component(mut self, component_name: &'static str) -> Self {
431 self.fetch_only_component_names.push(component_name);
432 self
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::bevy::plugins::persistence_plugin::PersistencePluginCore;
440 use crate::core::db::{
441 MockDatabaseConnection,
442 connection::{
443 BEVY_PERSISTENCE_DATABASE_BEVY_TYPE_FIELD, BEVY_PERSISTENCE_DATABASE_METADATA_FIELD,
444 BEVY_PERSISTENCE_DATABASE_VERSION_FIELD, DocumentKind, PersistenceError,
445 },
446 };
447 use crate::core::session::PersistenceSession;
448 use bevy::MinimalPlugins;
449 use bevy::prelude::App;
450 use bevy_persistence_database_derive::persist;
451 use futures::executor::block_on;
452 use serde_json::json;
453 use std::sync::Arc;
454
455 const TEST_STORE: &str = "test_store";
456
457 #[persist(component)]
458 struct A {
459 value: i32,
460 }
461
462 #[persist(component)]
463 struct B {
464 name: String,
465 }
466
467 #[test]
468 fn build_spec_with_dsl() {
469 let db = Arc::new(MockDatabaseConnection::new());
470 let query = PersistenceQuery::new()
471 .with_db(db)
472 .store(TEST_STORE)
473 .with::<A>()
474 .filter(A::value().gt(10).and(B::name().eq("test")));
475
476 let spec = query.build_spec();
477 assert!(spec.presence_with.contains(&<A as Persist>::name()));
478 assert!(spec.presence_with.contains(&<B as Persist>::name()));
479 assert!(spec.value_filters.is_some());
480 assert!(!spec.return_full_docs);
481 }
482
483 #[test]
484 fn build_spec_with_or_combiner() {
485 let db = Arc::new(MockDatabaseConnection::new());
486 let query = PersistenceQuery::new()
487 .with_db(db)
488 .store(TEST_STORE)
489 .filter(A::value().gt(10))
490 .or(B::name().eq("foo"));
491 let spec = query.build_spec();
492
493 assert!(spec.value_filters.is_some());
494 assert!(!spec.return_full_docs);
495 }
496
497 #[persist(component)]
498 struct Health {
499 value: i32,
500 }
501
502 #[persist(component)]
503 struct Position {
504 x: f32,
505 y: f32,
506 }
507
508 #[tokio::test]
509 async fn fetch_into_loads_new_entities() {
510 let mut mock_db = MockDatabaseConnection::new();
511 mock_db.expect_document_key_field().return_const("_key");
512 mock_db.expect_execute_documents().returning(|spec| {
513 assert!(
514 !spec.return_full_docs,
515 "narrow query should use partial projection"
516 );
517 assert!(spec.fetch_only.contains(&"Health"));
518 assert!(spec.fetch_only.contains(&"Position"));
519 Box::pin(async {
520 Ok(vec![
521 json!({
522 "_key":"k1",
523 BEVY_PERSISTENCE_DATABASE_METADATA_FIELD: {
524 BEVY_PERSISTENCE_DATABASE_VERSION_FIELD: 1,
525 BEVY_PERSISTENCE_DATABASE_BEVY_TYPE_FIELD: DocumentKind::Entity.as_str(),
526 },
527 "Health": 1,
528 "Position": {"x": 1.0, "y": 2.0},
529 }),
530 json!({
531 "_key":"k2",
532 BEVY_PERSISTENCE_DATABASE_METADATA_FIELD: {
533 BEVY_PERSISTENCE_DATABASE_VERSION_FIELD: 1,
534 BEVY_PERSISTENCE_DATABASE_BEVY_TYPE_FIELD: DocumentKind::Entity.as_str(),
535 },
536 "Health": 3,
537 "Position": {"x": 4.0, "y": 5.0},
538 }),
539 ])
540 })
541 });
542 mock_db
543 .expect_fetch_resource()
544 .returning(|_, _| Box::pin(async { Ok(None) }));
545
546 let db = Arc::new(mock_db) as Arc<dyn DatabaseConnection>;
547
548 let mut app = App::new();
549 app.add_plugins(MinimalPlugins);
550 app.add_plugins(PersistencePluginCore::new(db.clone()));
551
552 {
553 let mut session = app
554 .world_mut()
555 .resource_mut::<PersistenceSession>();
556 session.register_component::<Health>();
557 session.register_component::<Position>();
558 }
559
560 let query = PersistenceQuery::new()
561 .with_db(db)
562 .store(TEST_STORE)
563 .with::<Health>()
564 .with::<Position>();
565 let loaded = query.fetch_into(app.world_mut()).await;
566
567 assert_eq!(loaded.len(), 2);
568 }
569
570 #[tokio::test]
571 async fn fetch_into_unconstrained_uses_full_docs() {
572 let mut mock_db = MockDatabaseConnection::new();
573 mock_db.expect_document_key_field().return_const("_key");
574 mock_db.expect_execute_documents().returning(|spec| {
575 assert!(
576 spec.return_full_docs,
577 "unconstrained query should fetch full documents"
578 );
579 Box::pin(async { Ok(vec![]) })
580 });
581 mock_db
582 .expect_fetch_resource()
583 .returning(|_, _| Box::pin(async { Ok(None) }));
584
585 let db = Arc::new(mock_db) as Arc<dyn DatabaseConnection>;
586 let mut app = App::new();
587 app.add_plugins(MinimalPlugins);
588 app.add_plugins(PersistencePluginCore::new(db.clone()));
589
590 let query = PersistenceQuery::new().with_db(db).store(TEST_STORE);
591 let _ = query.fetch_into(app.world_mut()).await;
592 }
593
594 #[test]
595 fn build_spec_empty_filters() {
596 #[persist(component)]
597 struct Comp1;
598
599 let db = Arc::new(MockDatabaseConnection::new());
600 let query = PersistenceQuery::new().with_db(db).store(TEST_STORE).for_component::<Comp1>();
601 let spec = query.build_spec();
602
603 assert!(!spec.presence_with.is_empty());
604 assert!(spec.presence_without.is_empty());
605 assert!(!spec.return_full_docs);
606 assert_eq!(spec.fetch_only, vec!["Comp1"]);
607 }
608
609 #[test]
610 #[should_panic(expected = "query failed")]
611 fn fetch_ids_panics_on_error() {
612 let mut mock_db = MockDatabaseConnection::new();
613 mock_db.expect_execute_keys().returning(|_spec| {
614 Box::pin(async { Err(PersistenceError::General("db error".into())) })
615 });
616 let db = Arc::new(mock_db);
617 let query = PersistenceQuery::new().with_db(db).store(TEST_STORE);
618 block_on(query.fetch_ids());
619 }
620}