1use std::marker::PhantomData;
23use std::sync::Arc;
24
25use rainier_events::Dispatcher;
26use rainier_orm::sea_query::Value;
27use rainier_support::{Error, Result};
28
29use crate::connection::Database;
30use crate::criteria::Criteria;
31use crate::model::{Created, Creating, Deleted, Deleting, Model, Updated, Updating};
32use crate::pagination::Paginated;
33use crate::relation::{PivotQuery, RelationKey};
34use crate::row::Cell;
35use crate::statement;
36
37#[async_trait::async_trait]
44pub trait Repository<M: Model>: Send + Sync + 'static {
45 async fn all(&self) -> Result<Vec<M>>;
47
48 async fn find(&self, key: Value) -> Result<Option<M>>;
50
51 async fn find_by(&self, column: &str, value: Value) -> Result<Vec<M>>;
54
55 async fn first_by(&self, column: &str, value: Value) -> Result<Option<M>>;
57
58 async fn matching(&self, criteria: Criteria) -> Result<Vec<M>>;
60
61 async fn first_matching(&self, criteria: Criteria) -> Result<Option<M>>;
63
64 async fn count_matching(&self, criteria: Criteria) -> Result<u64>;
66
67 async fn paginate_matching(
69 &self,
70 criteria: Criteria,
71 page: u64,
72 per_page: u64,
73 ) -> Result<Paginated<M>>;
74
75 async fn create(&self, model: M) -> Result<M>;
77
78 async fn update(&self, model: &M) -> Result<u64>;
80
81 async fn upsert(
84 &self,
85 model: &M,
86 conflict_columns: &[&str],
87 update_columns: &[&str],
88 ) -> Result<u64>;
89
90 async fn delete(&self, key: Value) -> Result<u64>;
92
93 async fn delete_matching(&self, criteria: Criteria) -> Result<u64>;
95
96 async fn count_grouped(
106 &self,
107 column: &str,
108 criteria: Criteria,
109 ) -> Result<Vec<(RelationKey, u64)>> {
110 let mut counts: std::collections::HashMap<RelationKey, u64> =
111 std::collections::HashMap::new();
112
113 for model in self.matching(criteria).await? {
114 let Some(value) = model.value_of(column) else { continue };
115 *counts.entry(RelationKey::new(&value)).or_insert(0) += 1;
116 }
117 Ok(counts.into_iter().collect())
118 }
119
120 async fn pivot_links(&self, query: PivotQuery) -> Result<Vec<(Value, Value)>> {
129 let _ = query;
130 Ok(Vec::new())
131 }
132
133 async fn find_key(&self, key: impl Into<Value> + Send) -> Result<Option<M>>
137 where
138 Self: Sized,
139 {
140 self.find(key.into()).await
141 }
142
143 async fn find_or_fail(&self, key: impl Into<Value> + Send) -> Result<M>
146 where
147 Self: Sized,
148 {
149 self.find(key.into()).await?.ok_or_else(|| {
150 Error::not_found(format!("No {} matches the given key.", M::model_name()))
151 })
152 }
153
154 async fn find_by_route_key(&self, value: impl Into<Value> + Send) -> Result<Option<M>>
157 where
158 Self: Sized,
159 {
160 self.first_by(M::route_key_name(), value.into()).await
161 }
162
163 async fn where_eq(&self, column: &str, value: impl Into<Value> + Send) -> Result<Vec<M>>
165 where
166 Self: Sized,
167 {
168 self.find_by(column, value.into()).await
169 }
170
171 async fn count(&self) -> Result<u64>
173 where
174 Self: Sized,
175 {
176 self.count_matching(Criteria::new()).await
177 }
178
179 async fn paginate(&self, page: u64, per_page: u64) -> Result<Paginated<M>>
181 where
182 Self: Sized,
183 {
184 self.paginate_matching(Criteria::new(), page, per_page).await
185 }
186
187 async fn exists(&self, criteria: Criteria) -> Result<bool>
189 where
190 Self: Sized,
191 {
192 Ok(self.count_matching(criteria).await? > 0)
193 }
194}
195
196pub struct EntityRepository<M> {
198 db: Database,
199 events: Option<Arc<Dispatcher>>,
200 _model: PhantomData<fn() -> M>,
201}
202
203impl<M> Clone for EntityRepository<M> {
204 fn clone(&self) -> Self {
205 Self { db: self.db.clone(), events: self.events.clone(), _model: PhantomData }
206 }
207}
208
209impl<M: Model> EntityRepository<M> {
210 pub fn new(db: Database) -> Self {
212 Self { db, events: None, _model: PhantomData }
213 }
214
215 pub fn with_events(mut self, events: Arc<Dispatcher>) -> Self {
217 self.events = Some(events);
218 self
219 }
220
221 pub fn database(&self) -> &Database {
224 &self.db
225 }
226
227 async fn fire<E>(&self, build: impl FnOnce() -> E) -> Result<()>
232 where
233 E: Send + Sync + 'static,
234 {
235 let Some(events) = &self.events else { return Ok(()) };
236 if !events.has_listeners::<E>() {
237 return Ok(());
238 }
239 events.dispatch(build()).await
240 }
241}
242
243#[async_trait::async_trait]
244impl<M: Model> Repository<M> for EntityRepository<M> {
245 async fn all(&self) -> Result<Vec<M>> {
246 self.db.fetch_all(statement::select_all::<M>(self.db.dialect())).await
247 }
248
249 async fn find(&self, key: Value) -> Result<Option<M>> {
250 self.db.fetch_one(statement::select_by_pk::<M>(self.db.dialect(), key)).await
251 }
252
253 async fn find_by(&self, column: &str, value: Value) -> Result<Vec<M>> {
254 let prepared = statement::select_by_column::<M>(self.db.dialect(), column, value, None);
255 self.db.fetch_all(prepared).await
256 }
257
258 async fn first_by(&self, column: &str, value: Value) -> Result<Option<M>> {
259 let prepared = statement::select_by_column::<M>(self.db.dialect(), column, value, Some(1));
260 self.db.fetch_one(prepared).await
261 }
262
263 async fn matching(&self, criteria: Criteria) -> Result<Vec<M>> {
264 self.db.fetch_all(statement::select_matching::<M>(self.db.dialect(), &criteria)).await
265 }
266
267 async fn first_matching(&self, criteria: Criteria) -> Result<Option<M>> {
268 let criteria = criteria.limit(1);
269 self.db.fetch_one(statement::select_matching::<M>(self.db.dialect(), &criteria)).await
270 }
271
272 async fn count_matching(&self, criteria: Criteria) -> Result<u64> {
273 self.db.fetch_count(statement::count_matching::<M>(self.db.dialect(), &criteria)).await
274 }
275
276 async fn count_grouped(
277 &self,
278 column: &str,
279 criteria: Criteria,
280 ) -> Result<Vec<(RelationKey, u64)>> {
281 let Some(spec) = M::columns().iter().find(|spec| spec.name == column) else {
282 return Err(Error::internal(format!(
283 "`{}` has no column `{column}` to group by",
284 M::table()
285 )));
286 };
287
288 let prepared = statement::count_grouped::<M>(self.db.dialect(), column, &criteria);
289 let columns = vec![
290 crate::row::ColumnRequest::new(column, spec.ty),
291 crate::row::ColumnRequest::new("cnt", rainier_orm::ColumnType::BigInt),
292 ];
293
294 let rows = self.db.fetch(prepared, columns).await?;
295 Ok(rows
296 .iter()
297 .filter_map(|row| {
298 let key = row.cell(column).map(RelationKey::from_cell)?;
299 let count = row.cell("cnt").and_then(Cell::as_u64).unwrap_or(0);
300 Some((key, count))
301 })
302 .collect())
303 }
304
305 async fn pivot_links(&self, query: PivotQuery) -> Result<Vec<(Value, Value)>> {
306 if query.parent_keys.is_empty() {
307 return Ok(Vec::new());
308 }
309
310 let prepared = statement::select_pivot(self.db.dialect(), &query);
311 let key_type = M::columns()
315 .iter()
316 .find(|spec| spec.pk)
317 .map(|spec| spec.ty)
318 .unwrap_or(rainier_orm::ColumnType::BigInt);
319
320 let columns = vec![
321 crate::row::ColumnRequest::new(&query.parent_column, key_type),
322 crate::row::ColumnRequest::new(&query.related_column, key_type),
323 ];
324
325 let rows = self.db.fetch(prepared, columns).await?;
326 Ok(rows
327 .iter()
328 .filter_map(|row| {
329 let parent = row.cell(&query.parent_column).and_then(Cell::to_value)?;
330 let related = row.cell(&query.related_column).and_then(Cell::to_value)?;
331 Some((parent, related))
332 })
333 .collect())
334 }
335
336 async fn paginate_matching(
337 &self,
338 criteria: Criteria,
339 page: u64,
340 per_page: u64,
341 ) -> Result<Paginated<M>> {
342 let page = page.max(1);
343 let per_page = per_page.max(1);
344
345 let total = self.count_matching(criteria.clone()).await?;
346 if total == 0 {
347 return Ok(Paginated::empty(page, per_page));
349 }
350
351 let window = criteria.limit(per_page).offset((page - 1) * per_page);
352 let data = self.matching(window).await?;
353 Ok(Paginated::new(data, total, page, per_page))
354 }
355
356 async fn create(&self, model: M) -> Result<M> {
357 self.fire(|| Creating { model: model.clone() }).await?;
358
359 let assigned = statement::shard_key_for_insert::<M>(&model)
362 .and_then(|shard_key| self.db.connection().allocate_id(shard_key));
363
364 let prepared = statement::insert::<M>(self.db.dialect(), &model, assigned);
365 let outcome = self.db.execute(prepared).await?;
366
367 let id = assigned.map(|id| id as i64).unwrap_or(outcome.last_insert_id);
368
369 let stored = if assigned.is_none() && id > 0 && has_auto_increment_key::<M>() {
379 self.find(id.into()).await?.unwrap_or(model)
380 } else {
381 model
382 };
383
384 self.fire(|| Created { model: stored.clone(), id }).await?;
385 Ok(stored)
386 }
387
388 async fn update(&self, model: &M) -> Result<u64> {
389 self.fire(|| Updating { model: model.clone() }).await?;
390
391 let prepared = statement::update::<M>(self.db.dialect(), model);
392 let rows_affected = self.db.execute(prepared).await?.rows_affected;
393
394 self.fire(|| Updated { model: model.clone(), rows_affected }).await?;
395 Ok(rows_affected)
396 }
397
398 async fn upsert(
399 &self,
400 model: &M,
401 conflict_columns: &[&str],
402 update_columns: &[&str],
403 ) -> Result<u64> {
404 let prepared =
405 statement::upsert::<M>(self.db.dialect(), model, conflict_columns, update_columns);
406 Ok(self.db.execute(prepared).await?.rows_affected)
407 }
408
409 async fn delete(&self, key: Value) -> Result<u64> {
410 let described = describe(&key);
411 self.fire(|| Deleting::<M> { key: described.clone(), model: PhantomData }).await?;
412
413 let prepared = statement::delete_by_pk::<M>(self.db.dialect(), key);
414 let rows_affected = self.db.execute(prepared).await?.rows_affected;
415
416 self.fire(|| Deleted::<M> { key: described, rows_affected, model: PhantomData }).await?;
417 Ok(rows_affected)
418 }
419
420 async fn delete_matching(&self, criteria: Criteria) -> Result<u64> {
421 let prepared = statement::delete_matching::<M>(self.db.dialect(), &criteria);
422 Ok(self.db.execute(prepared).await?.rows_affected)
423 }
424}
425
426fn has_auto_increment_key<M: Model>() -> bool {
428 M::columns().iter().any(|column| column.name == M::primary_key() && column.auto_increment)
429}
430
431fn describe(key: &Value) -> String {
433 match key {
434 Value::String(Some(text)) => text.to_string(),
435 Value::BigInt(Some(n)) => n.to_string(),
436 Value::Int(Some(n)) => n.to_string(),
437 Value::BigUnsigned(Some(n)) => n.to_string(),
438 Value::Unsigned(Some(n)) => n.to_string(),
439 other => format!("{other:?}"),
440 }
441}
442
443impl<M> std::fmt::Debug for EntityRepository<M> {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 f.debug_struct("EntityRepository")
446 .field("model", &std::any::type_name::<M>())
447 .field("hooks", &self.events.is_some())
448 .finish()
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::row::OwnedRow;
456 use crate::testing::{fake_database, MemoryConnection};
457 use rainier_orm::{Dialect, ShardRoute};
458 use std::sync::atomic::{AtomicUsize, Ordering};
459 use std::sync::Mutex;
460
461 #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
462 #[orm(table = "posts")]
463 struct Post {
464 #[orm(pk, auto_increment)]
465 id: u64,
466 title: String,
467 published: bool,
468 }
469
470 impl Model for Post {}
471
472 #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
473 #[orm(table = "tokens")]
474 struct Token {
475 #[orm(pk, shard_key)]
476 id: u64,
477 #[orm(shard_key)]
478 user_id: u64,
479 hash: String,
480 }
481
482 impl Model for Token {}
483
484 fn row(id: u64, title: &str) -> OwnedRow {
485 OwnedRow::new().with("id", id).with("title", title).with("published", true)
486 }
487
488 fn count(n: i64) -> OwnedRow {
489 OwnedRow::new().with("cnt", n)
490 }
491
492 fn repository(connection: MemoryConnection) -> (EntityRepository<Post>, Arc<MemoryConnection>) {
493 let (db, handle) = fake_database(connection);
494 (EntityRepository::<Post>::new(db), handle)
495 }
496
497 #[test]
498 fn repository_calls_produce_send_futures() {
499 fn assert_send<T: Send>(_: T) {}
501
502 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite));
503 assert_send(async move {
504 let _ = posts.all().await;
505 });
506 }
507
508 #[tokio::test]
509 async fn all_decodes_every_row() {
510 let (posts, _) = repository(
511 MemoryConnection::new(Dialect::Sqlite).returning([row(1, "a"), row(2, "b")]),
512 );
513
514 let found = posts.all().await.unwrap();
515 assert_eq!(found.len(), 2);
516 assert_eq!(found[0], Post { id: 1, title: "a".into(), published: true });
517 }
518
519 #[tokio::test]
520 async fn find_returns_none_when_nothing_matches() {
521 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite));
522 assert!(posts.find(1_i64.into()).await.unwrap().is_none());
523 }
524
525 #[tokio::test]
526 async fn find_or_fail_reports_a_404_naming_the_model() {
527 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite));
528
529 let err = posts.find_or_fail(1_i64).await.unwrap_err();
530 assert_eq!(err.status(), 404);
531 assert!(err.message().contains("Post"), "{}", err.message());
532 }
533
534 #[tokio::test]
535 async fn find_or_fail_returns_the_row_when_it_exists() {
536 let (posts, _) =
537 repository(MemoryConnection::new(Dialect::Sqlite).returning([row(1, "found")]));
538 assert_eq!(posts.find_or_fail(1_i64).await.unwrap().title, "found");
539 }
540
541 #[tokio::test]
542 async fn first_by_limits_to_one_row() {
543 let (posts, connection) =
544 repository(MemoryConnection::new(Dialect::Sqlite).returning([row(1, "a")]));
545
546 posts.first_by("title", "a".into()).await.unwrap();
547 assert!(connection.last_statement().unwrap().contains("LIMIT"));
548 }
549
550 #[tokio::test]
551 async fn criteria_reach_the_generated_sql() {
552 let (posts, connection) = repository(MemoryConnection::new(Dialect::Sqlite));
553
554 posts
555 .matching(Criteria::new().where_eq("published", true).order_by_desc("id").limit(5))
556 .await
557 .unwrap();
558
559 let sql = connection.last_statement().unwrap();
560 assert!(sql.contains("published"), "{sql}");
561 assert!(sql.contains("ORDER BY"), "{sql}");
562 assert!(sql.contains("LIMIT"), "{sql}");
563 }
564
565 #[tokio::test]
566 async fn counting_ignores_the_criterias_paging() {
567 let (posts, connection) =
570 repository(MemoryConnection::new(Dialect::Sqlite).returning([count(42)]));
571
572 let total = posts
573 .count_matching(Criteria::new().where_eq("published", true).limit(5).offset(10))
574 .await
575 .unwrap();
576
577 assert_eq!(total, 42);
578 let sql = connection.last_statement().unwrap();
579 assert!(!sql.contains("LIMIT"), "the count must not be limited: {sql}");
580 assert!(sql.contains("published"), "but it must keep the filters: {sql}");
581 }
582
583 #[tokio::test]
584 async fn paginate_reports_the_window() {
585 let connection = MemoryConnection::new(Dialect::Sqlite)
586 .returning([count(25)])
587 .returning([row(11, "k"), row(12, "l")]);
588 let (posts, _) = repository(connection);
589
590 let page = posts.paginate(2, 10).await.unwrap();
591 assert_eq!(page.total, 25);
592 assert_eq!(page.current_page, 2);
593 assert_eq!(page.len(), 2);
594 assert_eq!(page.last_page(), 3);
595 assert_eq!(page.from(), Some(11));
596 }
597
598 #[tokio::test]
599 async fn paginating_no_results_skips_the_row_query() {
600 let (posts, connection) =
601 repository(MemoryConnection::new(Dialect::Sqlite).returning([count(0)]));
602
603 let page = posts.paginate(1, 10).await.unwrap();
604 assert!(page.is_empty());
605 assert_eq!(connection.statement_count(), 1);
606 }
607
608 #[tokio::test]
609 async fn exists_is_a_count() {
610 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite).returning([count(3)]));
611 assert!(posts.exists(Criteria::new()).await.unwrap());
612
613 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite).returning([count(0)]));
614 assert!(!posts.exists(Criteria::new()).await.unwrap());
615 }
616
617 #[tokio::test]
618 async fn create_reads_the_row_back_when_the_database_assigned_a_key() {
619 let connection =
620 MemoryConnection::new(Dialect::Sqlite).with_outcome(1, 7).returning([row(7, "stored")]);
621 let (posts, _) = repository(connection);
622
623 let created =
624 posts.create(Post { id: 0, title: "sent".into(), published: false }).await.unwrap();
625
626 assert_eq!(created.id, 7);
627 assert_eq!(created.title, "stored", "the stored row wins over the struct we sent");
628 }
629
630 #[tokio::test]
631 async fn create_does_not_re_read_a_model_with_an_application_assigned_key() {
632 #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
635 #[orm(table = "sessions")]
636 struct Session {
637 #[orm(pk)]
638 id: String,
639 user_id: u64,
640 }
641 impl Model for Session {}
642
643 let (db, connection) =
644 fake_database(MemoryConnection::new(Dialect::Sqlite).with_outcome(1, 99));
645 let sessions = EntityRepository::<Session>::new(db);
646
647 let created = sessions.create(Session { id: "abc".into(), user_id: 1 }).await.unwrap();
648
649 assert_eq!(created.id, "abc");
650 assert_eq!(connection.statement_count(), 1, "the insert only — no re-read");
651 assert!(connection.last_statement().unwrap().starts_with("INSERT INTO"));
652 }
653
654 #[tokio::test]
655 async fn a_sharded_insert_asks_the_connector_for_an_id_and_routes_to_it() {
656 let (db, connection) =
657 fake_database(MemoryConnection::new(Dialect::Sqlite).sharded("users").allocating(4242));
658 let tokens = EntityRepository::<Token>::new(db);
659
660 let created = tokens.create(Token { id: 0, user_id: 42, hash: "h".into() }).await.unwrap();
661
662 assert_eq!(created.id, 0);
664 assert_eq!(connection.last_route(), Some(ShardRoute::Key(4242)));
665 assert_eq!(connection.statement_count(), 1);
666 }
667
668 #[tokio::test]
669 async fn a_database_failure_surfaces() {
670 let (posts, _) = repository(MemoryConnection::new(Dialect::Sqlite).failing("disk full"));
671 let err = posts.all().await.unwrap_err();
672 assert!(err.message().contains("disk full"), "{}", err.message());
673 }
674
675 #[allow(clippy::type_complexity)]
678 fn with_hooks() -> (EntityRepository<Post>, Arc<Dispatcher>, Arc<MemoryConnection>) {
679 let (db, connection) = fake_database(
680 MemoryConnection::new(Dialect::Sqlite).with_outcome(1, 7).returning([row(7, "stored")]),
681 );
682 let events = Arc::new(Dispatcher::new());
683 (EntityRepository::<Post>::new(db).with_events(Arc::clone(&events)), events, connection)
684 }
685
686 #[tokio::test]
687 async fn create_fires_creating_then_created() {
688 let (posts, events, _) = with_hooks();
689 let log = Arc::new(Mutex::new(Vec::new()));
690
691 let sink = Arc::clone(&log);
692 events.listen(move |event: Arc<Creating<Post>>| {
693 let sink = Arc::clone(&sink);
694 async move {
695 sink.lock().unwrap().push(format!("creating:{}", event.model.title));
696 Ok(())
697 }
698 });
699
700 let sink = Arc::clone(&log);
701 events.listen(move |event: Arc<Created<Post>>| {
702 let sink = Arc::clone(&sink);
703 async move {
704 sink.lock().unwrap().push(format!("created:{}", event.id));
705 Ok(())
706 }
707 });
708
709 posts.create(Post { id: 0, title: "sent".into(), published: false }).await.unwrap();
710 assert_eq!(*log.lock().unwrap(), vec!["creating:sent", "created:7"]);
711 }
712
713 #[tokio::test]
714 async fn a_creating_listener_can_veto_the_insert() {
715 let (posts, events, connection) = with_hooks();
716 events.listen(|_: Arc<Creating<Post>>| async { Err(Error::unauthorized("read only")) });
717
718 let err = posts
719 .create(Post { id: 0, title: "blocked".into(), published: false })
720 .await
721 .unwrap_err();
722
723 assert_eq!(err.status(), 403);
724 assert_eq!(
725 connection.statement_count(),
726 0,
727 "a vetoed insert must never reach the database"
728 );
729 }
730
731 #[tokio::test]
732 async fn update_and_delete_fire_their_hooks() {
733 let (posts, events, _) = with_hooks();
734 let seen = Arc::new(AtomicUsize::new(0));
735
736 let counter = Arc::clone(&seen);
737 events.listen(move |_: Arc<Updating<Post>>| {
738 let counter = Arc::clone(&counter);
739 async move {
740 counter.fetch_add(1, Ordering::SeqCst);
741 Ok(())
742 }
743 });
744 let counter = Arc::clone(&seen);
745 events.listen(move |_: Arc<Updated<Post>>| {
746 let counter = Arc::clone(&counter);
747 async move {
748 counter.fetch_add(10, Ordering::SeqCst);
749 Ok(())
750 }
751 });
752 let counter = Arc::clone(&seen);
753 events.listen(move |_: Arc<Deleting<Post>>| {
754 let counter = Arc::clone(&counter);
755 async move {
756 counter.fetch_add(100, Ordering::SeqCst);
757 Ok(())
758 }
759 });
760 let counter = Arc::clone(&seen);
761 events.listen(move |event: Arc<Deleted<Post>>| {
762 let counter = Arc::clone(&counter);
763 async move {
764 assert_eq!(event.key, "1");
765 counter.fetch_add(1000, Ordering::SeqCst);
766 Ok(())
767 }
768 });
769
770 posts.update(&Post { id: 1, title: "x".into(), published: true }).await.unwrap();
771 posts.delete(1_i64.into()).await.unwrap();
772
773 assert_eq!(seen.load(Ordering::SeqCst), 1111);
774 }
775
776 #[tokio::test]
777 async fn hooks_are_skipped_entirely_when_nothing_listens() {
778 let (posts, _, connection) = with_hooks();
779 posts.create(Post { id: 0, title: "x".into(), published: false }).await.unwrap();
782 assert!(connection.statement_count() >= 1);
783 }
784
785 #[tokio::test]
786 async fn the_contract_is_object_safe() {
787 let (db, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
788 let boxed: Arc<dyn Repository<Post>> = Arc::new(EntityRepository::<Post>::new(db));
789 assert!(boxed.all().await.unwrap().is_empty());
790 }
791}