1use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13#[cfg(feature = "permission")]
14use crate::access::permission::{PermissionAction, PermissionContext};
15#[cfg(feature = "sql-parser")]
16use crate::access::security::{DdlGuard, DdlValidationResult};
17#[cfg(all(feature = "sql-parser", feature = "permission"))]
18use crate::access::sql_parser::SqlParser;
19#[cfg(feature = "sql-parser")]
20use crate::access::sql_parser::is_ddl_operation;
21use crate::database::pool::db_pool::{DatabaseConnection, DbConnection, DbPool, DbPoolInner};
22use crate::foundation::error::{DbError, DbResult};
23#[cfg(feature = "metrics")]
24use crate::observability::metrics::MetricsCollector;
25use async_trait::async_trait;
26
27use sea_orm::{ConnectionTrait, DatabaseTransaction, ExecResult, TransactionTrait};
29use tokio::sync::Mutex;
30
31struct SessionState {
35 transaction: Option<Arc<DatabaseTransaction>>,
41
42 last_write: Option<Instant>,
44}
45
46pub struct Session {
48 connection: Option<DbConnection>,
50
51 pool: Arc<DbPool>,
53
54 pool_inner: Arc<DbPoolInner>,
56
57 role: String,
59
60 #[cfg(feature = "permission")]
62 permission_ctx: PermissionContext,
63
64 state: Mutex<SessionState>,
66
67 #[cfg(feature = "metrics")]
69 metrics_collector: Option<Arc<MetricsCollector>>,
70}
71
72impl Session {
73 pub(crate) fn new(connection: DbConnection, pool: Arc<DbPool>, pool_inner: Arc<DbPoolInner>, role: String) -> Self {
75 #[cfg(feature = "permission")]
76 let permission_ctx = PermissionContext::new(role.clone(), pool_inner.policy_cache.clone());
77
78 #[cfg(feature = "metrics")]
79 let metrics = pool_inner.metrics_collector.clone();
80
81 Session {
82 connection: Some(connection),
83 pool,
84 pool_inner,
85 role,
86 #[cfg(feature = "permission")]
87 permission_ctx,
88 state: Mutex::new(SessionState {
89 transaction: None,
90 last_write: None,
91 }),
92 #[cfg(feature = "metrics")]
93 metrics_collector: metrics,
94 }
95 }
96
97 pub fn role(&self) -> &str {
99 &self.role
100 }
101
102 #[cfg(feature = "permission")]
104 pub fn permission_ctx(&self) -> &PermissionContext {
105 &self.permission_ctx
106 }
107
108 pub async fn mark_write(&self) {
110 let mut state = self.state.lock().await;
111 state.last_write = Some(Instant::now());
112 }
113
114 #[cfg(feature = "permission")]
116 pub async fn check_permission(&self, table: &str, operation: &PermissionAction) -> Result<(), DbError> {
117 if self.role == self.pool_inner.admin_role {
119 return Ok(());
120 }
121
122 if self.permission_ctx.check_table_access(table, operation).await {
123 Ok(())
124 } else {
125 Err(permission_denied(operation, table))
126 }
127 }
128
129 pub async fn is_in_transaction(&self) -> bool {
131 let state = self.state.lock().await;
132 state.transaction.is_some()
133 }
134
135 pub async fn begin_transaction(&self) -> Result<(), DbError> {
140 {
142 let state = self.state.lock().await;
143 if state.transaction.is_some() {
144 return Err(DbError::Transaction("Already in transaction".to_string()));
145 }
146 }
147
148 let conn = self.connection()?;
150 let transaction = conn
151 .begin()
152 .await
153 .map_err(|e| DbError::Transaction(format!("Failed to begin transaction: {}", e)))?;
154
155 let mut state = self.state.lock().await;
157 if state.transaction.is_some() {
158 let _ = transaction.rollback().await;
160 return Err(DbError::Transaction(
161 "Already in transaction (concurrent begin detected)".to_string(),
162 ));
163 }
164 state.transaction = Some(Arc::new(transaction));
165 Ok(())
166 }
167
168 pub async fn commit(&self) -> Result<(), DbError> {
177 let transaction_arc = {
179 let mut state = self.state.lock().await;
180 state
181 .transaction
182 .take()
183 .ok_or_else(|| DbError::Transaction("No active transaction to commit".to_string()))?
184 };
185
186 let transaction = Arc::try_unwrap(transaction_arc).map_err(|_| {
188 DbError::Transaction("Cannot commit: transaction is in use by a concurrent query".to_string())
189 })?;
190
191 transaction
193 .commit()
194 .await
195 .map_err(|e| DbError::Transaction(e.to_string()))?;
196
197 let mut state = self.state.lock().await;
199 state.last_write = None;
200 Ok(())
201 }
202
203 pub async fn rollback(&self) -> Result<(), DbError> {
212 let transaction_arc = {
214 let mut state = self.state.lock().await;
215 if state.transaction.is_none() {
216 return Err(DbError::Transaction("Not in transaction".to_string()));
217 }
218 state
219 .transaction
220 .take()
221 .ok_or_else(|| DbError::Transaction("No active transaction to rollback".to_string()))?
222 };
223
224 let transaction = Arc::try_unwrap(transaction_arc).map_err(|_| {
226 DbError::Transaction("Cannot rollback: transaction is in use by a concurrent query".to_string())
227 })?;
228
229 transaction
231 .rollback()
232 .await
233 .map_err(|e| DbError::Transaction(format!("Failed to rollback transaction: {}", e)))?;
234
235 Ok(())
236 }
237
238 pub async fn should_use_master(&self) -> bool {
240 let state = self.state.lock().await;
241 if state.transaction.is_some() {
243 return true;
244 }
245
246 state
248 .last_write
249 .map(|t| t.elapsed() < Duration::from_secs(5))
250 .unwrap_or(false)
251 }
252
253 pub fn connection(&self) -> Result<&DatabaseConnection, DbError> {
263 self.connection
264 .as_ref()
265 .ok_or_else(|| DbError::Config("Connection not available - Session may have been invalidated".to_string()))?
266 .as_sea_orm()
267 }
268
269 #[allow(dead_code)]
273 #[cfg(feature = "migration")]
274 pub fn create_migration_executor(
275 &self,
276 db_type: crate::foundation::config::DatabaseType,
277 ) -> Result<super::MigrationExecutor, DbError> {
278 let conn = self.connection()?.clone();
279 Ok(super::MigrationExecutor::new(conn, db_type))
280 }
281
282 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(db.role = %self.role)))]
284 pub async fn execute_raw(&self, sql: &str) -> DbResult<ExecResult> {
285 #[cfg(feature = "sql-parser")]
286 {
287 if is_ddl_operation(sql) {
289 return Err(DbError::Permission(
290 "DDL operations are not allowed in this context".to_string(),
291 ));
292 }
293 }
294
295 #[cfg(not(feature = "sql-parser"))]
296 {
297 let _ = sql;
298 Err(DbError::Permission(
299 "execute_raw requires the sql-parser feature to be enabled".to_string(),
300 ))
301 }
302
303 #[cfg(feature = "sql-parser")]
304 {
305 #[cfg(all(feature = "sql-parser", feature = "permission"))]
306 {
307 let parser = SqlParser::shared().await;
309 match parser.parse_operation_async(sql).await {
310 Ok(Some((table_name, action))) => {
311 if table_name.is_empty() || is_invalid_table_name(&table_name) {
312 return Err(DbError::Permission(
313 "Failed to extract table name for permission checking".to_string(),
314 ));
315 }
316 if self.role == self.pool_inner.admin_role {
319 } else if !self.permission_ctx.check_table_access(&table_name, &action).await {
321 return Err(permission_denied(&action, &table_name));
322 }
323 }
324 Ok(None) => {
325 return Err(DbError::Permission(
328 "SQL statement requires a valid table name for permission checking".to_string(),
329 ));
330 }
331 Err(_) => {
332 return Err(DbError::Permission(
334 "Failed to parse SQL statement for permission checking".to_string(),
335 ));
336 }
337 }
338 }
339
340 let tx_opt: Option<Arc<DatabaseTransaction>> = {
342 let state = self.state.lock().await;
343 state.transaction.clone()
344 };
345 if let Some(tx) = tx_opt {
346 return tx.execute_unprepared(sql).await.map_err(DbError::Connection);
347 }
348
349 let conn = self.connection()?;
350 conn.execute_unprepared(sql).await.map_err(DbError::Connection)
351 }
352 }
353
354 pub async fn execute_raw_ddl(&self, sql: &str) -> DbResult<ExecResult> {
371 if self.role != self.pool_inner.admin_role {
373 return Err(DbError::Permission(format!(
374 "DDL operations are only allowed for admin role. Current role: '{}', Admin role: '{}'",
375 self.role, self.pool_inner.admin_role
376 )));
377 }
378
379 #[cfg(feature = "sql-parser")]
381 {
382 let guard = DdlGuard::new();
383 match guard.validate(sql) {
384 Ok(DdlValidationResult::Allowed) => {
385 }
387 Ok(DdlValidationResult::Forbidden(reason)) => {
388 return Err(DbError::Permission(format!("DDL operation not allowed: {}", reason)));
389 }
390 Ok(DdlValidationResult::ParseError(error)) => {
391 return Err(DbError::Config(format!("Failed to parse DDL SQL: {}", error)));
392 }
393 Err(error) => {
394 return Err(DbError::Config(format!("DDL validation error: {}", error)));
395 }
396 }
397 }
398
399 let conn = self.connection()?;
401 conn.execute_unprepared(sql).await.map_err(DbError::Connection)
402 }
403
404 #[cfg(feature = "duckdb")]
417 pub async fn execute_duckdb(&self, sql: &str) -> DbResult<Vec<crate::database::pool::DuckDbRow>> {
418 #[cfg(feature = "sql-parser")]
420 {
421 if is_ddl_operation(sql) {
422 return Err(DbError::Permission(
423 "DDL operations are not allowed in DuckDB query context".to_string(),
424 ));
425 }
426 }
427
428 #[cfg(not(feature = "sql-parser"))]
429 {
430 let _ = sql;
431 Err(DbError::Permission(
432 "execute_duckdb requires the sql-parser feature to be enabled for security checks".to_string(),
433 ))
434 }
435
436 #[cfg(feature = "sql-parser")]
437 {
438 #[cfg(all(feature = "sql-parser", feature = "permission"))]
439 {
440 let parser = SqlParser::shared().await;
441 match parser.parse_operation_async(sql).await {
442 Ok(Some((table_name, action))) => {
443 if table_name.is_empty() || is_invalid_table_name(&table_name) {
444 return Err(DbError::Permission(
445 "Failed to extract table name for permission checking".to_string(),
446 ));
447 }
448 if self.role != self.pool_inner.admin_role
449 && !self.permission_ctx.check_table_access(&table_name, &action).await
450 {
451 return Err(permission_denied(&action, &table_name));
452 }
453 }
454 Ok(None) => {
455 if self.role != self.pool_inner.admin_role {
459 return Err(DbError::Permission(
460 "SQL statement requires a valid table name for permission checking".to_string(),
461 ));
462 }
463 }
464 Err(_) => {
465 return Err(DbError::Permission(
466 "Failed to parse SQL statement for permission checking".to_string(),
467 ));
468 }
469 }
470 }
471
472 let conn = self
473 .connection
474 .as_ref()
475 .ok_or_else(|| DbError::Config("Connection not available".to_string()))?;
476 let duck_conn = conn.as_duckdb()?;
477 duck_conn.query(sql).await
478 }
479 }
480
481 #[cfg(feature = "duckdb")]
494 pub async fn execute_duckdb_raw(&self, sql: &str) -> DbResult<crate::database::pool::DuckDbExecResult> {
495 #[cfg(feature = "sql-parser")]
499 {
500 if is_ddl_operation(sql) {
501 if self.role == self.pool_inner.admin_role {
502 let guard = DdlGuard::new();
505 match guard.validate(sql) {
506 Ok(DdlValidationResult::Allowed) => {
507 let conn = self
508 .connection
509 .as_ref()
510 .ok_or_else(|| DbError::Config("Connection not available".to_string()))?;
511 let duck_conn = conn.as_duckdb()?;
512 return duck_conn.execute(sql).await;
513 }
514 Ok(DdlValidationResult::Forbidden(reason)) => {
515 return Err(DbError::Permission(format!("DDL operation not allowed: {}", reason)));
516 }
517 Ok(DdlValidationResult::ParseError(error)) => {
518 return Err(DbError::Config(format!("Failed to parse DDL SQL: {}", error)));
519 }
520 Err(error) => {
521 return Err(DbError::Config(format!("DDL validation error: {}", error)));
522 }
523 }
524 } else {
525 return Err(DbError::Permission(format!(
526 "DDL operations are only allowed for admin role in DuckDB context. Current role: '{}', Admin role: '{}'",
527 self.role, self.pool_inner.admin_role
528 )));
529 }
530 }
531 }
532
533 #[cfg(not(feature = "sql-parser"))]
534 {
535 let _ = sql;
536 Err(DbError::Permission(
537 "execute_duckdb_raw requires the sql-parser feature to be enabled for security checks".to_string(),
538 ))
539 }
540
541 #[cfg(feature = "sql-parser")]
542 {
543 #[cfg(all(feature = "sql-parser", feature = "permission"))]
544 {
545 let parser = SqlParser::shared().await;
546 match parser.parse_operation_async(sql).await {
547 Ok(Some((table_name, action))) => {
548 if table_name.is_empty() || is_invalid_table_name(&table_name) {
549 return Err(DbError::Permission(
550 "Failed to extract table name for permission checking".to_string(),
551 ));
552 }
553 if self.role != self.pool_inner.admin_role
554 && !self.permission_ctx.check_table_access(&table_name, &action).await
555 {
556 return Err(permission_denied(&action, &table_name));
557 }
558 }
559 Ok(None) => {
560 if self.role != self.pool_inner.admin_role {
563 return Err(DbError::Permission(
564 "SQL statement requires a valid table name for permission checking".to_string(),
565 ));
566 }
567 }
568 Err(_) => {
569 return Err(DbError::Permission(
570 "Failed to parse SQL statement for permission checking".to_string(),
571 ));
572 }
573 }
574 }
575
576 let conn = self
577 .connection
578 .as_ref()
579 .ok_or_else(|| DbError::Config("Connection not available".to_string()))?;
580 let duck_conn = conn.as_duckdb()?;
581 duck_conn.execute(sql).await
582 }
583 }
584
585 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(db.role = %self.role)))]
587 pub async fn execute(&self, sql: &str) -> DbResult<ExecResult> {
588 #[cfg(feature = "sql-parser")]
590 check_ddl_operation(sql)?;
591
592 #[cfg(feature = "permission")]
593 {
594 let start = Instant::now();
595 let parsed = parse_sql_for_permission(sql).await?;
597 match parsed {
598 Some((table_name, action)) => {
599 if table_name.is_empty() || is_invalid_table_name(&table_name) {
601 return Err(DbError::Permission(
602 "Failed to extract table name for permission checking".to_string(),
603 ));
604 }
605 self.check_permission(&table_name, &action).await?;
607 let result = self.execute_raw(sql).await?;
609 self.record_metrics_and_mark_write(&action, start).await;
611 Ok(result)
612 }
613 None => {
614 let result = self.execute_raw(sql).await?;
617 Ok(result)
618 }
619 }
620 }
621
622 #[cfg(not(feature = "permission"))]
623 {
624 let result = self.execute_raw(sql).await?;
626 Ok(result)
627 }
628 }
629
630 #[cfg(feature = "permission")]
632 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, operation), fields(db.role = %self.role)))]
633 pub async fn execute_with_operation(&self, sql: &str, operation: &PermissionAction) -> DbResult<ExecResult> {
634 let start = Instant::now();
635
636 #[cfg(feature = "sql-parser")]
637 {
638 if is_ddl_operation(sql) {
640 return Err(DbError::Permission(
641 "DDL operations are not allowed in this context".to_string(),
642 ));
643 }
644 }
645
646 let table_name = extract_table_name(sql);
648
649 #[cfg(feature = "permission")]
651 {
652 if !table_name.is_empty() && !self.permission_ctx.check_table_access(&table_name, operation).await {
653 return Err(permission_denied(operation, &table_name));
654 }
655 }
656
657 let result = self.execute_raw(sql).await?;
659
660 let duration = start.elapsed();
662 self.record_query_metrics(&format!("{:?}", operation), duration, true);
663
664 #[cfg(feature = "permission")]
666 {
667 if is_write_action(operation) {
668 self.mark_write().await;
669 }
670 }
671
672 Ok(result)
673 }
674
675 pub async fn batch_execute(&self, sqls: Vec<&str>) -> DbResult<Vec<DbResult<ExecResult>>> {
685 let mut results = Vec::new();
686
687 for sql in sqls {
688 let result = self.execute(sql).await;
689 results.push(result);
690 }
691
692 Ok(results)
693 }
694
695 pub async fn batch_execute_in_transaction(&self, sqls: Vec<&str>) -> DbResult<Vec<ExecResult>> {
707 self.begin_transaction().await?;
708
709 let mut results = Vec::new();
710 let mut last_error = None;
711
712 for sql in sqls {
713 match self.execute_raw(sql).await {
714 Ok(result) => results.push(result),
715 Err(e) => {
716 last_error = Some(e);
717 break;
718 }
719 }
720 }
721
722 if let Some(error) = last_error {
723 self.rollback().await?;
724 Err(error)
725 } else {
726 self.commit().await?;
727 Ok(results)
728 }
729 }
730
731 #[cfg(all(feature = "metrics", feature = "permission"))]
733 fn record_query_metrics(&self, query_type: &str, duration: Duration, success: bool) {
734 if let Some(metrics) = &self.metrics_collector {
735 metrics.record_query(query_type, duration, success, None);
736 }
737 }
738
739 #[cfg(all(not(feature = "metrics"), feature = "permission"))]
741 fn record_query_metrics(&self, _query_type: &str, _duration: Duration, _success: bool) {
742 }
744
745 #[cfg(feature = "permission")]
750 async fn record_metrics_and_mark_write(&self, action: &PermissionAction, start: Instant) {
751 let duration = start.elapsed();
752 self.record_query_metrics(&format!("{:?}", action), duration, true);
753 if is_write_action(action) {
754 self.mark_write().await;
755 }
756 }
757
758 pub async fn check_table_permission(&self, _table_name: &str, _operation: &str) -> DbResult<()> {
762 #[cfg(feature = "permission")]
763 {
764 let action = match _operation {
765 "INSERT" => PermissionAction::Insert,
766 "SELECT" => PermissionAction::Select,
767 "UPDATE" => PermissionAction::Update,
768 "DELETE" => PermissionAction::Delete,
769 _ => return Err(DbError::Permission(format!("Unknown operation: {}", _operation))),
770 };
771
772 if self.role == self.pool_inner.admin_role {
774 } else if !self.permission_ctx.check_table_access(_table_name, &action).await {
776 return Err(permission_denied(_operation, _table_name));
777 }
778 }
779 Ok(())
780 }
781
782 #[cfg(feature = "metrics")]
784 pub fn record_metric(&self, operation: &str, table_name: &str, success: bool) {
785 if let Some(metrics) = &self.metrics_collector {
786 let bytes = Some(table_name.len() as u64);
788 metrics.record_query(operation, std::time::Duration::from_millis(0), success, bytes);
789 }
790 }
791}
792
793#[cfg(feature = "permission")]
794fn is_invalid_table_name(table_name: &str) -> bool {
795 let table_name = table_name.trim();
796 if table_name.is_empty() {
797 return true;
798 }
799
800 for part in table_name.split('.') {
801 let part = part.trim();
802 if part.is_empty() {
803 return true;
804 }
805
806 let unquoted = part
807 .strip_prefix('"')
808 .and_then(|s| s.strip_suffix('"'))
809 .or_else(|| part.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
810 .or_else(|| part.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
811 .unwrap_or(part)
812 .trim();
813
814 if unquoted.is_empty() {
815 return true;
816 }
817 }
818
819 false
820}
821
822#[cfg(feature = "permission")]
827fn permission_denied(action: &(impl std::fmt::Display + ?Sized), table: &(impl std::fmt::Display + ?Sized)) -> DbError {
828 DbError::Permission(format!("Permission denied for {} on {}", action, table))
829}
830
831#[cfg(feature = "permission")]
833fn is_write_action(action: &PermissionAction) -> bool {
834 matches!(
835 action,
836 PermissionAction::Insert | PermissionAction::Update | PermissionAction::Delete
837 )
838}
839
840#[cfg(feature = "sql-parser")]
844fn check_ddl_operation(sql: &str) -> DbResult<()> {
845 if is_ddl_operation(sql) {
846 return Err(DbError::Permission(
847 "DDL operations are not allowed in this context".to_string(),
848 ));
849 }
850 Ok(())
851}
852
853impl Drop for Session {
854 fn drop(&mut self) {
855 if let Some(conn) = self.connection.take() {
857 self.pool.release_connection(conn);
858 }
859 }
860}
861
862#[cfg(feature = "permission")]
864fn extract_table_name(sql: &str) -> String {
865 let sql_upper = sql.to_uppercase();
867
868 if sql_upper.contains("FROM ") {
869 if let Some(start) = sql_upper.find("FROM ") {
870 let rest = &sql[start + 5..];
871 if let Some(end) = rest.find(|c| [' ', ',', ';', '(', ')'].contains(&c)) {
872 return rest[..end].trim().to_string();
873 } else {
874 return rest.trim().to_string();
875 }
876 }
877 }
878
879 if sql_upper.contains("INTO ") {
880 if let Some(start) = sql_upper.find("INTO ") {
881 let rest = &sql[start + 5..];
882 if let Some(end) = rest.find(|c| [' ', '(', ';'].contains(&c)) {
883 return rest[..end].trim().to_string();
884 } else {
885 return rest.trim().to_string();
886 }
887 }
888 }
889
890 if sql_upper.contains("UPDATE ") {
891 if let Some(start) = sql_upper.find("UPDATE ") {
892 let rest = &sql[start + 7..];
893 if let Some(end) = rest.find(|c| [' ', ';'].contains(&c)) {
894 return rest[..end].trim().to_string();
895 } else {
896 return rest.trim().to_string();
897 }
898 }
899 }
900
901 String::new()
902}
903
904#[cfg(all(feature = "permission", not(feature = "sql-parser")))]
905fn parse_table_and_action(sql: &str) -> (String, PermissionAction) {
906 let table_name = extract_table_name(sql);
907 let sql_upper = sql.trim_start().to_uppercase();
908 let action = if sql_upper.starts_with("INSERT") {
909 PermissionAction::Insert
910 } else if sql_upper.starts_with("UPDATE") {
911 PermissionAction::Update
912 } else if sql_upper.starts_with("DELETE") {
913 PermissionAction::Delete
914 } else {
915 PermissionAction::Select
916 };
917
918 (table_name, action)
919}
920
921#[cfg(feature = "permission")]
928async fn parse_sql_for_permission(sql: &str) -> DbResult<Option<(String, PermissionAction)>> {
929 #[cfg(feature = "sql-parser")]
930 {
931 let parser = SqlParser::shared().await;
932 match parser.parse_operation_async(sql).await {
933 Ok(Some((table, action))) => Ok(Some((table, action))),
934 Ok(None) => Ok(None),
935 Err(_) => Ok(None),
936 }
937 }
938 #[cfg(not(feature = "sql-parser"))]
939 {
940 Ok(Some(parse_table_and_action(sql)))
941 }
942}
943
944#[async_trait]
946impl super::DatabaseSession for Session {
947 async fn execute(&self, sql: &str) -> crate::DbResult<ExecResult> {
948 Ok(self.execute(sql).await?)
949 }
950
951 async fn execute_raw(&self, sql: &str) -> crate::DbResult<ExecResult> {
952 Ok(self.execute_raw(sql).await?)
953 }
954
955 async fn execute_raw_ddl(&self, sql: &str) -> crate::DbResult<ExecResult> {
956 Ok(self.execute_raw_ddl(sql).await?)
957 }
958
959 async fn begin_transaction(&self) -> crate::DbResult<()> {
960 Ok(self.begin_transaction().await?)
961 }
962
963 async fn commit(&self) -> crate::DbResult<()> {
964 Ok(self.commit().await?)
965 }
966
967 async fn rollback(&self) -> crate::DbResult<()> {
968 Ok(self.rollback().await?)
969 }
970
971 fn role(&self) -> &str {
972 self.role()
973 }
974
975 async fn is_in_transaction(&self) -> bool {
976 self.is_in_transaction().await
977 }
978}