linkrs_server/graph_service/
batch_exec.rs1use std::collections::HashMap;
2
3use linkrs_api::api_core::QueryResult;
4
5use crate::query::parser::ast::Stmt;
6use crate::storage::{
7 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
8};
9
10use super::error::GraphServiceError;
11use super::GraphService;
12
13impl<
14 S: StorageClient
15 + StorageSchemaContextOps
16 + StorageSyncContextOps
17 + StorageOperationContextOps
18 + Clone
19 + crate::storage::AutoCommitBatchOps
20 + 'static,
21 > GraphService<S>
22{
23 pub async fn execute_batch(
26 &self,
27 session_id: i64,
28 statements: &[String],
29 parameters: Option<HashMap<String, linkrs_core::Value>>,
30 session_variables: Option<HashMap<String, linkrs_core::Value>>,
31 ) -> Vec<Result<QueryResult, GraphServiceError>> {
32 let Some(session) = self.session_manager.find_session(session_id) else {
33 return statements
34 .iter()
35 .map(|_| {
36 Err(GraphServiceError::new(format!(
37 "Invalid session ID: {session_id}"
38 )))
39 })
40 .collect();
41 };
42 session.charge();
43
44 let mut results: Vec<Option<Result<QueryResult, GraphServiceError>>> =
45 vec![None; statements.len()];
46 let mut batch_indices: Vec<usize> = Vec::new();
47 let mut batch_statements: Vec<String> = Vec::new();
48 for (index, stmt) in statements.iter().enumerate() {
49 match Self::parse_command(stmt) {
50 Err(parse_error) => results[index] = Some(Err(parse_error)),
51 Ok(Some(parsed)) => match parsed.ast.stmt() {
52 Stmt::AssignVariable(_) => {
53 results[index] = Some(
54 self.execute_with_params(
55 session_id,
56 stmt,
57 parameters.clone(),
58 session_variables.clone(),
59 )
60 .await,
61 );
62 }
63 _ => {
64 results[index] = Some(Err(GraphServiceError::new(
65 "Transaction commands are not supported in batch execution",
66 )));
67 }
68 },
69 Ok(None) => {
70 batch_indices.push(index);
71 batch_statements.push(stmt.clone());
72 }
73 }
74 }
75
76 if session.current_transaction().is_some() || !session.is_auto_commit() {
77 for (index, stmt) in batch_statements.iter().enumerate() {
78 results[batch_indices[index]] = Some(
79 self.execute_with_params(
80 session_id,
81 stmt,
82 parameters.clone(),
83 session_variables.clone(),
84 )
85 .await,
86 );
87 }
88 return finalize_batch_outcomes(results);
89 }
90
91 let space_id = session.space().map(|s| s.id as i64).unwrap_or(0);
92 let username = session.user();
93 let (permitted, denied) = self.partition_permitted(&username, space_id, &batch_statements);
94
95 let query_request = linkrs_api::api_core::QueryRequest {
96 isolation_level: None,
97 space_id: session.space().map(|s| s.id),
98 space_name: session.space().map(|s| s.name),
99 auto_commit: true,
100 transaction_id: None,
101 parameters,
102 session_variables,
103 query_id: None,
104 parsed_statement: None,
105 consistency: Default::default(),
106 };
107 let outcomes = self
108 .query_api
109 .write()
110 .execute_batch(&permitted, query_request);
111
112 merge_batch_outcomes(&mut results, &batch_indices, &denied, outcomes);
113 finalize_batch_outcomes(results)
114 }
115
116 fn partition_permitted(
118 &self,
119 username: &str,
120 space_id: i64,
121 batch_statements: &[String],
122 ) -> (Vec<String>, Vec<(usize, String)>) {
123 use super::context::QueryExecutionContext;
124 let mut denied: Vec<(usize, String)> = Vec::new();
125 let mut permitted: Vec<String> = Vec::with_capacity(batch_statements.len());
126 for (index, stmt) in batch_statements.iter().enumerate() {
127 let context = QueryExecutionContext {
128 stmt,
129 parsed_ast: None,
130 space_id,
131 parameters: None,
132 session_variables: None,
133 };
134 if let Err(e) = self.check_query_permission(username, &context) {
135 denied.push((index, e.message().to_string()));
136 continue;
137 }
138 permitted.push(stmt.clone());
139 }
140 (permitted, denied)
141 }
142}
143
144impl<
145 S: StorageClient
146 + StorageSchemaContextOps
147 + StorageSyncContextOps
148 + StorageOperationContextOps
149 + Clone
150 + crate::storage::AutoCommitBatchOps
151 + crate::storage::AutoCommitGroupOps
152 + 'static,
153 > GraphService<S>
154{
155 pub async fn execute_batch_grouped(
158 &self,
159 session_id: i64,
160 statements: &[String],
161 group_size: usize,
162 ) -> Vec<Result<QueryResult, GraphServiceError>> {
163 let Some(session) = self.session_manager.find_session(session_id) else {
164 return statements
165 .iter()
166 .map(|_| {
167 Err(GraphServiceError::new(format!(
168 "Invalid session ID: {session_id}"
169 )))
170 })
171 .collect();
172 };
173 session.charge();
174
175 let mut results: Vec<Option<Result<QueryResult, GraphServiceError>>> =
176 vec![None; statements.len()];
177 let mut batch_indices: Vec<usize> = Vec::new();
178 let mut batch_statements: Vec<String> = Vec::new();
179 for (index, stmt) in statements.iter().enumerate() {
180 match Self::parse_command(stmt) {
181 Err(parse_error) => results[index] = Some(Err(parse_error)),
182 Ok(Some(parsed)) => match parsed.ast.stmt() {
183 Stmt::AssignVariable(_) => {
184 results[index] = Some(self.execute(session_id, stmt).await);
185 }
186 _ => {
187 results[index] = Some(Err(GraphServiceError::new(
188 "Transaction commands are not supported in batch execution",
189 )));
190 }
191 },
192 Ok(None) => {
193 batch_indices.push(index);
194 batch_statements.push(stmt.clone());
195 }
196 }
197 }
198
199 if session.current_transaction().is_some() || !session.is_auto_commit() {
200 for (index, stmt) in batch_statements.iter().enumerate() {
201 results[batch_indices[index]] = Some(self.execute(session_id, stmt).await);
202 }
203 return finalize_batch_outcomes(results);
204 }
205
206 let space_id = session.space().map(|s| s.id as i64).unwrap_or(0);
207 let username = session.user();
208 let (permitted, denied) = self.partition_permitted(&username, space_id, &batch_statements);
209
210 let query_request = linkrs_api::api_core::QueryRequest {
211 isolation_level: None,
212 space_id: session.space().map(|s| s.id),
213 space_name: session.space().map(|s| s.name),
214 auto_commit: true,
215 transaction_id: None,
216 parameters: None,
217 session_variables: None,
218 query_id: None,
219 parsed_statement: None,
220 consistency: Default::default(),
221 };
222 let outcomes =
223 self.query_api
224 .write()
225 .execute_batch_grouped(&permitted, query_request, group_size);
226
227 merge_batch_outcomes(&mut results, &batch_indices, &denied, outcomes);
228 finalize_batch_outcomes(results)
229 }
230}
231
232fn finalize_batch_outcomes(
234 results: Vec<Option<Result<QueryResult, GraphServiceError>>>,
235) -> Vec<Result<QueryResult, GraphServiceError>> {
236 results
237 .into_iter()
238 .map(|slot| slot.unwrap_or_else(|| Err(GraphServiceError::new("Batch outcome missing"))))
239 .collect()
240}
241
242fn merge_batch_outcomes(
244 results: &mut [Option<Result<QueryResult, GraphServiceError>>],
245 batch_indices: &[usize],
246 denied: &[(usize, String)],
247 outcomes: Vec<Result<linkrs_api::api_core::QueryResult, linkrs_api::api_core::CoreError>>,
248) {
249 let mut permitted_outcomes = outcomes.into_iter();
250 for (batch_pos, original_index) in batch_indices.iter().enumerate() {
251 if let Some((_, error)) = denied.iter().find(|(i, _)| *i == batch_pos) {
252 results[*original_index] = Some(Err(GraphServiceError::new(error.clone())));
253 continue;
254 }
255 match permitted_outcomes.next() {
256 Some(Ok(result)) => {
257 results[*original_index] = Some(Ok(result));
258 }
259 Some(Err(error)) => {
260 results[*original_index] = Some(Err(GraphServiceError::from_core_error(error)));
261 }
262 None => {
263 results[*original_index] =
264 Some(Err(GraphServiceError::new("Batch outcome missing")));
265 }
266 }
267 }
268}