|
1 | 1 | use super::{ArgsTuple, InvalidReducerArguments, ReducerArgs, ReducerCallResult, ReducerId, ReducerOutcome, Scheduler}; |
| 2 | +use crate::client::messages::{OneOffQueryResponseMessage, SerializableMessage}; |
2 | 3 | use crate::client::{ClientActorId, ClientConnectionSender}; |
3 | 4 | use crate::database_logger::{LogLevel, Record}; |
4 | 5 | use crate::db::datastore::locking_tx_datastore::MutTxId; |
@@ -32,6 +33,7 @@ use spacetimedb_data_structures::map::{HashCollectionExt as _, IntMap}; |
32 | 33 | use spacetimedb_execution::pipelined::PipelinedProject; |
33 | 34 | use spacetimedb_lib::db::raw_def::v9::Lifecycle; |
34 | 35 | use spacetimedb_lib::identity::{AuthCtx, RequestId}; |
| 36 | +use spacetimedb_lib::metrics::ExecutionMetrics; |
35 | 37 | use spacetimedb_lib::ConnectionId; |
36 | 38 | use spacetimedb_lib::Timestamp; |
37 | 39 | use spacetimedb_primitives::TableId; |
@@ -993,60 +995,106 @@ impl ModuleHost { |
993 | 995 | ) |
994 | 996 | } |
995 | 997 |
|
| 998 | + /// Execute a one-off query and send the results to the given client. |
| 999 | + /// This only returns an error if there is a db-level problem. |
| 1000 | + /// An error with the query itself will be sent to the client. |
996 | 1001 | #[tracing::instrument(level = "trace", skip_all)] |
997 | | - pub fn one_off_query<F: WebsocketFormat>( |
| 1002 | + pub async fn one_off_query<F: WebsocketFormat>( |
998 | 1003 | &self, |
999 | 1004 | caller_identity: Identity, |
1000 | 1005 | query: String, |
1001 | | - ) -> Result<OneOffTable<F>, anyhow::Error> { |
| 1006 | + client: Arc<ClientConnectionSender>, |
| 1007 | + message_id: Vec<u8>, |
| 1008 | + timer: Instant, |
| 1009 | + // We take this because we only have a way to convert with the concrete types (Bsatn and Json) |
| 1010 | + into_message: impl FnOnce(OneOffQueryResponseMessage<F>) -> SerializableMessage + Send + 'static, |
| 1011 | + ) -> Result<(), anyhow::Error> { |
1002 | 1012 | let replica_ctx = self.replica_ctx(); |
1003 | | - let db = &replica_ctx.relational_db; |
| 1013 | + let db = replica_ctx.relational_db.clone(); |
| 1014 | + let subscriptions = replica_ctx.subscriptions.clone(); |
1004 | 1015 | let auth = AuthCtx::new(replica_ctx.owner_identity, caller_identity); |
1005 | 1016 | log::debug!("One-off query: {query}"); |
| 1017 | + let metrics = asyncify(move || { |
| 1018 | + db.with_read_only(Workload::Sql, |tx| { |
| 1019 | + // We wrap the actual query in a closure so we can use ? to handle errors without making |
| 1020 | + // the entire transaction abort with an error. |
| 1021 | + let result: Result<(OneOffTable<F>, ExecutionMetrics), anyhow::Error> = (|| { |
| 1022 | + let tx = SchemaViewer::new(tx, &auth); |
| 1023 | + |
| 1024 | + let ( |
| 1025 | + // A query may compile down to several plans. |
| 1026 | + // This happens when there are multiple RLS rules per table. |
| 1027 | + // The original query is the union of these plans. |
| 1028 | + plans, |
| 1029 | + _, |
| 1030 | + table_name, |
| 1031 | + _, |
| 1032 | + ) = compile_subscription(&query, &tx, &auth)?; |
| 1033 | + |
| 1034 | + // Optimize each fragment |
| 1035 | + let optimized = plans |
| 1036 | + .into_iter() |
| 1037 | + .map(|plan| plan.optimize()) |
| 1038 | + .collect::<Result<Vec<_>, _>>()?; |
| 1039 | + |
| 1040 | + check_row_limit( |
| 1041 | + &optimized, |
| 1042 | + &db, |
| 1043 | + &tx, |
| 1044 | + // Estimate the number of rows this query will scan |
| 1045 | + |plan, tx| estimate_rows_scanned(tx, plan), |
| 1046 | + &auth, |
| 1047 | + )?; |
| 1048 | + |
| 1049 | + let optimized = optimized |
| 1050 | + .into_iter() |
| 1051 | + // Convert into something we can execute |
| 1052 | + .map(PipelinedProject::from) |
| 1053 | + .collect::<Vec<_>>(); |
| 1054 | + |
| 1055 | + // Execute the union and return the results |
| 1056 | + execute_plan::<_, F>(&optimized, &DeltaTx::from(&*tx)) |
| 1057 | + .map(|(rows, _, metrics)| (OneOffTable { table_name, rows }, metrics)) |
| 1058 | + .context("One-off queries are not allowed to modify the database") |
| 1059 | + })(); |
| 1060 | + |
| 1061 | + let total_host_execution_duration = timer.elapsed().into(); |
| 1062 | + let (message, metrics): (SerializableMessage, Option<ExecutionMetrics>) = match result { |
| 1063 | + Ok((rows, metrics)) => ( |
| 1064 | + into_message(OneOffQueryResponseMessage { |
| 1065 | + message_id, |
| 1066 | + error: None, |
| 1067 | + results: vec![rows], |
| 1068 | + total_host_execution_duration, |
| 1069 | + }), |
| 1070 | + Some(metrics), |
| 1071 | + ), |
| 1072 | + Err(err) => ( |
| 1073 | + into_message(OneOffQueryResponseMessage { |
| 1074 | + message_id, |
| 1075 | + error: Some(format!("{}", err)), |
| 1076 | + results: vec![], |
| 1077 | + total_host_execution_duration, |
| 1078 | + }), |
| 1079 | + None, |
| 1080 | + ), |
| 1081 | + }; |
| 1082 | + |
| 1083 | + subscriptions.send_client_message(client, message, tx)?; |
| 1084 | + Ok::<Option<ExecutionMetrics>, anyhow::Error>(metrics) |
| 1085 | + }) |
| 1086 | + }) |
| 1087 | + .await?; |
| 1088 | + |
| 1089 | + if let Some(metrics) = metrics { |
| 1090 | + // Record the metrics for the one-off query |
| 1091 | + replica_ctx |
| 1092 | + .relational_db |
| 1093 | + .exec_counters_for(WorkloadType::Sql) |
| 1094 | + .record(&metrics); |
| 1095 | + } |
1006 | 1096 |
|
1007 | | - let (rows, metrics) = db.with_read_only(Workload::Sql, |tx| { |
1008 | | - let tx = SchemaViewer::new(tx, &auth); |
1009 | | - |
1010 | | - let ( |
1011 | | - // A query may compile down to several plans. |
1012 | | - // This happens when there are multiple RLS rules per table. |
1013 | | - // The original query is the union of these plans. |
1014 | | - plans, |
1015 | | - _, |
1016 | | - table_name, |
1017 | | - _, |
1018 | | - ) = compile_subscription(&query, &tx, &auth)?; |
1019 | | - |
1020 | | - // Optimize each fragment |
1021 | | - let optimized = plans |
1022 | | - .into_iter() |
1023 | | - .map(|plan| plan.optimize()) |
1024 | | - .collect::<Result<Vec<_>, _>>()?; |
1025 | | - |
1026 | | - check_row_limit( |
1027 | | - &optimized, |
1028 | | - db, |
1029 | | - &tx, |
1030 | | - // Estimate the number of rows this query will scan |
1031 | | - |plan, tx| estimate_rows_scanned(tx, plan), |
1032 | | - &auth, |
1033 | | - )?; |
1034 | | - |
1035 | | - let optimized = optimized |
1036 | | - .into_iter() |
1037 | | - // Convert into something we can execute |
1038 | | - .map(PipelinedProject::from) |
1039 | | - .collect::<Vec<_>>(); |
1040 | | - |
1041 | | - // Execute the union and return the results |
1042 | | - execute_plan::<_, F>(&optimized, &DeltaTx::from(&*tx)) |
1043 | | - .map(|(rows, _, metrics)| (OneOffTable { table_name, rows }, metrics)) |
1044 | | - .context("One-off queries are not allowed to modify the database") |
1045 | | - })?; |
1046 | | - |
1047 | | - db.exec_counters_for(WorkloadType::Sql).record(&metrics); |
1048 | | - |
1049 | | - Ok(rows) |
| 1097 | + Ok(()) |
1050 | 1098 | } |
1051 | 1099 |
|
1052 | 1100 | /// FIXME(jgilles): this is a temporary workaround for deleting not currently being supported |
|
0 commit comments